diff --git a/.github/workflows/pd-test-build-deploy.yaml b/.github/workflows/pd-test-build-deploy.yaml index ce28d79e9a5..babebb5a918 100644 --- a/.github/workflows/pd-test-build-deploy.yaml +++ b/.github/workflows/pd-test-build-deploy.yaml @@ -164,7 +164,7 @@ jobs: run: | make -C protocol-designer NODE_ENV=development - name: 'upload github artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: 'pd-artifact' path: protocol-designer/dist @@ -197,7 +197,7 @@ jobs: const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) buildComplexEnvVars(core, context) - name: 'download PD build' - uses: 'actions/download-artifact@v3' + uses: 'actions/download-artifact@v4' with: name: pd-artifact path: ./dist diff --git a/abr-testing/Pipfile b/abr-testing/Pipfile index 613ca5203f7..8dd89eb5d9b 100644 --- a/abr-testing/Pipfile +++ b/abr-testing/Pipfile @@ -20,6 +20,7 @@ pandas = "*" pandas-stubs = "*" paramiko = "*" prettier = "*" +pydantic = "==2.9.0" [dev-packages] atomicwrites = "==1.4.1" diff --git a/abr-testing/Pipfile.lock b/abr-testing/Pipfile.lock index a2f82b44925..7793a4e0e4c 100644 --- a/abr-testing/Pipfile.lock +++ b/abr-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f773f4880fa452637eeaf5e1aebee4ca6a1dc34907f588e0c6f71f0f222dc725" + "sha256": "ae4207dab4efffe35a5a0ad2539aa2ee746a3c6c604f54c18ccfcd14d5425537" }, "pipfile-spec": 6, "requires": { @@ -30,85 +30,85 @@ }, "aiohttp": { "hashes": [ - "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0", - "sha256:02c13415b5732fb6ee7ff64583a5e6ed1c57aa68f17d2bda79c04888dfdc2769", - "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5", - "sha256:04814571cb72d65a6899db6099e377ed00710bf2e3eafd2985166f2918beaf59", - "sha256:0580f2e12de2138f34debcd5d88894786453a76e98febaf3e8fe5db62d01c9bf", - "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985", - "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50", - "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299", - "sha256:0dc49f42422163efb7e6f1df2636fe3db72713f6cd94688e339dbe33fe06d61d", - "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab", - "sha256:15fccaf62a4889527539ecb86834084ecf6e9ea70588efde86e8bc775e0e7542", - "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b", - "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b", - "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838", - "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683", - "sha256:3bbbfff4c679c64e6e23cb213f57cc2c9165c9a65d63717108a644eb5a7398df", - "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d", - "sha256:40d1c7a7f750b5648642586ba7206999650208dbe5afbcc5284bcec6579c9b91", - "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9", - "sha256:47ad15a65fb41c570cd0ad9a9ff8012489e68176e7207ec7b82a0940dddfd8be", - "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c", - "sha256:49c7dbbc1a559ae14fc48387a115b7d4bbc84b4a2c3b9299c31696953c2a5219", - "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4", - "sha256:4cca22a61b7fe45da8fc73c3443150c3608750bbe27641fc7558ec5117b27fdf", - "sha256:4cfce37f31f20800a6a6620ce2cdd6737b82e42e06e6e9bd1b36f546feb3c44f", - "sha256:502a1464ccbc800b4b1995b302efaf426e8763fadf185e933c2931df7db9a199", - "sha256:53bf2097e05c2accc166c142a2090e4c6fd86581bde3fd9b2d3f9e93dda66ac1", - "sha256:593c114a2221444f30749cc5e5f4012488f56bd14de2af44fe23e1e9894a9c60", - "sha256:5d6958671b296febe7f5f859bea581a21c1d05430d1bbdcf2b393599b1cdce77", - "sha256:5ef359ebc6949e3a34c65ce20230fae70920714367c63afd80ea0c2702902ccf", - "sha256:613e5169f8ae77b1933e42e418a95931fb4867b2991fc311430b15901ed67079", - "sha256:61b9bae80ed1f338c42f57c16918853dc51775fb5cb61da70d590de14d8b5fb4", - "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46", - "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8", - "sha256:65e55ca7debae8faaffee0ebb4b47a51b4075f01e9b641c31e554fd376595c6c", - "sha256:68386d78743e6570f054fe7949d6cb37ef2b672b4d3405ce91fafa996f7d9b4d", - "sha256:68ff6f48b51bd78ea92b31079817aff539f6c8fc80b6b8d6ca347d7c02384e33", - "sha256:6ab29b8a0beb6f8eaf1e5049252cfe74adbaafd39ba91e10f18caeb0e99ffb34", - "sha256:77ae58586930ee6b2b6f696c82cf8e78c8016ec4795c53e36718365f6959dc82", - "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b", - "sha256:78153314f26d5abef3239b4a9af20c229c6f3ecb97d4c1c01b22c4f87669820c", - "sha256:7852bbcb4d0d2f0c4d583f40c3bc750ee033265d80598d0f9cb6f372baa6b836", - "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69", - "sha256:7f3dc0e330575f5b134918976a645e79adf333c0a1439dcf6899a80776c9ab39", - "sha256:80886dac673ceaef499de2f393fc80bb4481a129e6cb29e624a12e3296cc088f", - "sha256:811f23b3351ca532af598405db1093f018edf81368e689d1b508c57dcc6b6a32", - "sha256:86a5dfcc39309470bd7b68c591d84056d195428d5d2e0b5ccadfbaf25b026ebc", - "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52", - "sha256:8cc5203b817b748adccb07f36390feb730b1bc5f56683445bfe924fc270b8816", - "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1", - "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec", - "sha256:98283b94cc0e11c73acaf1c9698dea80c830ca476492c0fe2622bd931f34b487", - "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0", - "sha256:99b7920e7165be5a9e9a3a7f1b680f06f68ff0d0328ff4079e5163990d046767", - "sha256:9bca390cb247dbfaec3c664326e034ef23882c3f3bfa5fbf0b56cad0320aaca5", - "sha256:9e2e576caec5c6a6b93f41626c9c02fc87cd91538b81a3670b2e04452a63def6", - "sha256:9ef405356ba989fb57f84cac66f7b0260772836191ccefbb987f414bcd2979d9", - "sha256:a55d2ad345684e7c3dd2c20d2f9572e9e1d5446d57200ff630e6ede7612e307f", - "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138", - "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e", - "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf", - "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109", - "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408", - "sha256:beb39a6d60a709ae3fb3516a1581777e7e8b76933bb88c8f4420d875bb0267c6", - "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d", - "sha256:c49a76c1038c2dd116fa443eba26bbb8e6c37e924e2513574856de3b6516be99", - "sha256:c5532f0441fc09c119e1dca18fbc0687e64fbeb45aa4d6a87211ceaee50a74c4", - "sha256:c6b9e6d7e41656d78e37ce754813fa44b455c3d0d0dced2a047def7dc5570b74", - "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc", - "sha256:cbad88a61fa743c5d283ad501b01c153820734118b65aee2bd7dbb735475ce0d", - "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5", - "sha256:db1d0b28fcb7f1d35600150c3e4b490775251dea70f894bf15c678fdd84eda6a", - "sha256:ddf5f7d877615f6a1e75971bfa5ac88609af3b74796ff3e06879e8422729fd01", - "sha256:e44a9a3c053b90c6f09b1bb4edd880959f5328cf63052503f892c41ea786d99f", - "sha256:efb15a17a12497685304b2d976cb4939e55137df7b09fa53f1b6a023f01fcb4e", - "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3" + "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", + "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33", + "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", + "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", + "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9", + "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e", + "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350", + "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226", + "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", + "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a", + "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6", + "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add", + "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", + "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", + "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", + "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e", + "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", + "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1", + "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c", + "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", + "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5", + "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e", + "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", + "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3", + "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438", + "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", + "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3", + "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", + "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", + "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2", + "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", + "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c", + "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55", + "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", + "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e", + "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1", + "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", + "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", + "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", + "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", + "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1", + "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", + "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", + "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", + "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", + "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8", + "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", + "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", + "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", + "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", + "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", + "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef", + "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28", + "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", + "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", + "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", + "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", + "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", + "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", + "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", + "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745", + "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", + "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", + "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43", + "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", + "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", + "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd", + "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d", + "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", + "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", + "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", + "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", + "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", + "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f", + "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", + "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e" ], "markers": "python_version >= '3.9'", - "version": "==3.11.10" + "version": "==3.11.11" }, "aionotify": { "hashes": [ @@ -120,11 +120,11 @@ }, "aiosignal": { "hashes": [ - "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", - "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", + "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54" ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" + "markers": "python_version >= '3.9'", + "version": "==1.3.2" }, "annotated-types": { "hashes": [ @@ -147,16 +147,16 @@ "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.11'", "version": "==5.0.1" }, "attrs": { "hashes": [ - "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", - "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" + "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", + "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308" ], - "markers": "python_version >= '3.7'", - "version": "==24.2.0" + "markers": "python_version >= '3.8'", + "version": "==24.3.0" }, "bcrypt": { "hashes": [ @@ -199,11 +199,11 @@ }, "certifi": { "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", + "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" ], "markers": "python_version >= '3.6'", - "version": "==2024.8.30" + "version": "==2024.12.14" }, "cffi": { "hashes": [ @@ -275,127 +275,114 @@ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], - "markers": "python_version >= '3.8'", + "markers": "platform_python_implementation != 'PyPy'", "version": "==1.17.1" }, "charset-normalizer": { "hashes": [ - "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", - "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", - "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", - "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", - "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", - "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", - "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", - "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", - "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", - "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", - "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", - "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", - "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", - "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", - "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", - "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", - "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", - "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", - "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", - "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", - "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", - "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", - "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", - "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", - "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", - "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", - "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", - "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", - "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", - "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", - "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", - "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", - "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", - "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", - "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", - "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", - "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", - "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", - "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", - "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", - "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", - "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", - "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", - "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", - "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", - "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", - "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", - "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", - "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", - "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", - "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", - "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", - "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", - "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", - "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", - "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", - "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", - "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", - "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", - "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", - "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", - "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", - "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", - "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", - "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", - "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", - "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", - "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", - "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", - "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", - "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", - "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", - "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", - "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", - "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", - "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", - "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", - "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", - "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", - "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", - "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", - "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", - "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", - "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", - "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", - "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", - "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", - "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", - "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", - "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", - "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", - "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", - "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", - "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", - "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", - "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", - "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", - "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", - "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", - "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", - "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", - "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", - "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", - "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", - "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.4.0" + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "version": "==8.1.8" }, "cryptography": { "hashes": [ @@ -435,7 +422,7 @@ "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==1.2.2" }, "frozenlist": { @@ -546,20 +533,20 @@ }, "google-api-python-client": { "hashes": [ - "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17", - "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad" + "sha256:0b0231db106324c659bf8b85f390391c00da57a60ebc4271e33def7aac198c75", + "sha256:2ee342d0967ad1cedec43ccd7699671d94bff151e1f06833ea81303f9a6d86fd" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.154.0" + "version": "==2.157.0" }, "google-auth": { "hashes": [ - "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", - "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1" + "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00", + "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0" ], "markers": "python_version >= '3.7'", - "version": "==2.36.0" + "version": "==2.37.0" }, "google-auth-httplib2": { "hashes": [ @@ -616,19 +603,11 @@ }, "jsonschema": { "hashes": [ - "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", - "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" ], - "markers": "python_version >= '3.8'", - "version": "==4.23.0" - }, - "jsonschema-specifications": { - "hashes": [ - "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", - "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" - ], - "markers": "python_version >= '3.9'", - "version": "==2024.10.1" + "markers": "python_version >= '3.7'", + "version": "==4.17.3" }, "msgpack": { "hashes": [ @@ -689,7 +668,7 @@ "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], - "markers": "python_version >= '3.8'", + "markers": "platform_system != 'Windows'", "version": "==1.0.8" }, "multidict": { @@ -829,7 +808,7 @@ "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" ], - "markers": "python_version >= '3.9'", + "markers": "python_version < '3.11'", "version": "==1.26.4" }, "oauth2client": { @@ -1049,20 +1028,20 @@ }, "protobuf": { "hashes": [ - "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c", - "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331", - "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", - "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", - "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", - "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853", - "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57", - "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", - "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", - "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", - "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18" + "sha256:13d6d617a2a9e0e82a88113d7191a1baa1e42c2cc6f5f1398d3b054c8e7e714a", + "sha256:2d2e674c58a06311c8e99e74be43e7f3a8d1e2b2fdf845eaa347fbd866f23355", + "sha256:36000f97ea1e76e8398a3f02936aac2a5d2b111aae9920ec1b769fc4a222c4d9", + "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e", + "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9", + "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb", + "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e", + "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e", + "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851", + "sha256:e621a98c0201a7c8afe89d9646859859be97cb22b8bf1d8eacfd90d5bda2eb19", + "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181" ], "markers": "python_version >= '3.8'", - "version": "==5.29.1" + "version": "==5.29.2" }, "pyasn1": { "hashes": [ @@ -1090,125 +1069,115 @@ }, "pydantic": { "hashes": [ - "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", - "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9" + "sha256:c7a8a9fdf7d100afa49647eae340e2d23efa382466a8d177efcd1381e9be5598", + "sha256:f66a7073abd93214a20c5f7b32d56843137a7a2e70d02111f3be287035c45370" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.10.3" + "version": "==2.9.0" }, "pydantic-core": { "hashes": [ - "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", - "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", - "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", - "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", - "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", - "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", - "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", - "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", - "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", - "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", - "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", - "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", - "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", - "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", - "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", - "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", - "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", - "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", - "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", - "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", - "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", - "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", - "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", - "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", - "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", - "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", - "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", - "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", - "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", - "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", - "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", - "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", - "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", - "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", - "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", - "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", - "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", - "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", - "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", - "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", - "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", - "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", - "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", - "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", - "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", - "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", - "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", - "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", - "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", - "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", - "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", - "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", - "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", - "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", - "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", - "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", - "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", - "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", - "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", - "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", - "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", - "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", - "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", - "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", - "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", - "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", - "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", - "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", - "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", - "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", - "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", - "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", - "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", - "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", - "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", - "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", - "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", - "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", - "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", - "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", - "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", - "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", - "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", - "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", - "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", - "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", - "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", - "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", - "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", - "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", - "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", - "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", - "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", - "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", - "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", - "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", - "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", - "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", - "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", - "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b" + "sha256:0102e49ac7d2df3379ef8d658d3bc59d3d769b0bdb17da189b75efa861fc07b4", + "sha256:0123655fedacf035ab10c23450163c2f65a4174f2bb034b188240a6cf06bb123", + "sha256:043ef8469f72609c4c3a5e06a07a1f713d53df4d53112c6d49207c0bd3c3bd9b", + "sha256:0448b81c3dfcde439551bb04a9f41d7627f676b12701865c8a2574bcea034437", + "sha256:05b366fb8fe3d8683b11ac35fa08947d7b92be78ec64e3277d03bd7f9b7cda79", + "sha256:07049ec9306ec64e955b2e7c40c8d77dd78ea89adb97a2013d0b6e055c5ee4c5", + "sha256:084414ffe9a85a52940b49631321d636dadf3576c30259607b75516d131fecd0", + "sha256:086c5db95157dc84c63ff9d96ebb8856f47ce113c86b61065a066f8efbe80acf", + "sha256:12625e69b1199e94b0ae1c9a95d000484ce9f0182f9965a26572f054b1537e44", + "sha256:16b25a4a120a2bb7dab51b81e3d9f3cde4f9a4456566c403ed29ac81bf49744f", + "sha256:19f1352fe4b248cae22a89268720fc74e83f008057a652894f08fa931e77dced", + "sha256:1a2ab4f410f4b886de53b6bddf5dd6f337915a29dd9f22f20f3099659536b2f6", + "sha256:1c7b81beaf7c7ebde978377dc53679c6cba0e946426fc7ade54251dfe24a7604", + "sha256:1cf842265a3a820ebc6388b963ead065f5ce8f2068ac4e1c713ef77a67b71f7c", + "sha256:1eb37f7d6a8001c0f86dc8ff2ee8d08291a536d76e49e78cda8587bb54d8b329", + "sha256:23af245b8f2f4ee9e2c99cb3f93d0e22fb5c16df3f2f643f5a8da5caff12a653", + "sha256:257d6a410a0d8aeb50b4283dea39bb79b14303e0fab0f2b9d617701331ed1515", + "sha256:276ae78153a94b664e700ac362587c73b84399bd1145e135287513442e7dfbc7", + "sha256:2b1a195efd347ede8bcf723e932300292eb13a9d2a3c1f84eb8f37cbbc905b7f", + "sha256:329a721253c7e4cbd7aad4a377745fbcc0607f9d72a3cc2102dd40519be75ed2", + "sha256:358331e21a897151e54d58e08d0219acf98ebb14c567267a87e971f3d2a3be59", + "sha256:3649bd3ae6a8ebea7dc381afb7f3c6db237fc7cebd05c8ac36ca8a4187b03b30", + "sha256:3713dc093d5048bfaedbba7a8dbc53e74c44a140d45ede020dc347dda18daf3f", + "sha256:3ef71ec876fcc4d3bbf2ae81961959e8d62f8d74a83d116668409c224012e3af", + "sha256:41ae8537ad371ec018e3c5da0eb3f3e40ee1011eb9be1da7f965357c4623c501", + "sha256:4a801c5e1e13272e0909c520708122496647d1279d252c9e6e07dac216accc41", + "sha256:4c83c64d05ffbbe12d4e8498ab72bdb05bcc1026340a4a597dc647a13c1605ec", + "sha256:4cebb9794f67266d65e7e4cbe5dcf063e29fc7b81c79dc9475bd476d9534150e", + "sha256:5668b3173bb0b2e65020b60d83f5910a7224027232c9f5dc05a71a1deac9f960", + "sha256:56e6a12ec8d7679f41b3750ffa426d22b44ef97be226a9bab00a03365f217b2b", + "sha256:582871902e1902b3c8e9b2c347f32a792a07094110c1bca6c2ea89b90150caac", + "sha256:5c8aa40f6ca803f95b1c1c5aeaee6237b9e879e4dfb46ad713229a63651a95fb", + "sha256:5d813fd871b3d5c3005157622ee102e8908ad6011ec915a18bd8fde673c4360e", + "sha256:5dd0ec5f514ed40e49bf961d49cf1bc2c72e9b50f29a163b2cc9030c6742aa73", + "sha256:5f3cf3721eaf8741cffaf092487f1ca80831202ce91672776b02b875580e174a", + "sha256:6294907eaaccf71c076abdd1c7954e272efa39bb043161b4b8aa1cd76a16ce43", + "sha256:64d094ea1aa97c6ded4748d40886076a931a8bf6f61b6e43e4a1041769c39dd2", + "sha256:6650a7bbe17a2717167e3e23c186849bae5cef35d38949549f1c116031b2b3aa", + "sha256:67b6655311b00581914aba481729971b88bb8bc7996206590700a3ac85e457b8", + "sha256:6b06c5d4e8701ac2ba99a2ef835e4e1b187d41095a9c619c5b185c9068ed2a49", + "sha256:6ce883906810b4c3bd90e0ada1f9e808d9ecf1c5f0b60c6b8831d6100bcc7dd6", + "sha256:6db09153d8438425e98cdc9a289c5fade04a5d2128faff8f227c459da21b9703", + "sha256:6f80fba4af0cb1d2344869d56430e304a51396b70d46b91a55ed4959993c0589", + "sha256:743e5811b0c377eb830150d675b0847a74a44d4ad5ab8845923d5b3a756d8100", + "sha256:753294d42fb072aa1775bfe1a2ba1012427376718fa4c72de52005a3d2a22178", + "sha256:7568f682c06f10f30ef643a1e8eec4afeecdafde5c4af1b574c6df079e96f96c", + "sha256:7706e15cdbf42f8fab1e6425247dfa98f4a6f8c63746c995d6a2017f78e619ae", + "sha256:785e7f517ebb9890813d31cb5d328fa5eda825bb205065cde760b3150e4de1f7", + "sha256:7a05c0240f6c711eb381ac392de987ee974fa9336071fb697768dfdb151345ce", + "sha256:7ce7eaf9a98680b4312b7cebcdd9352531c43db00fca586115845df388f3c465", + "sha256:7ce8e26b86a91e305858e018afc7a6e932f17428b1eaa60154bd1f7ee888b5f8", + "sha256:7d0324a35ab436c9d768753cbc3c47a865a2cbc0757066cb864747baa61f6ece", + "sha256:7e9b24cca4037a561422bf5dc52b38d390fb61f7bfff64053ce1b72f6938e6b2", + "sha256:810ca06cca91de9107718dc83d9ac4d2e86efd6c02cba49a190abcaf33fb0472", + "sha256:820f6ee5c06bc868335e3b6e42d7ef41f50dfb3ea32fbd523ab679d10d8741c0", + "sha256:82764c0bd697159fe9947ad59b6db6d7329e88505c8f98990eb07e84cc0a5d81", + "sha256:8ae65fdfb8a841556b52935dfd4c3f79132dc5253b12c0061b96415208f4d622", + "sha256:8d5b0ff3218858859910295df6953d7bafac3a48d5cd18f4e3ed9999efd2245f", + "sha256:95d6bf449a1ac81de562d65d180af5d8c19672793c81877a2eda8fde5d08f2fd", + "sha256:964c7aa318da542cdcc60d4a648377ffe1a2ef0eb1e996026c7f74507b720a78", + "sha256:96ef39add33ff58cd4c112cbac076726b96b98bb8f1e7f7595288dcfb2f10b57", + "sha256:a6612c2a844043e4d10a8324c54cdff0042c558eef30bd705770793d70b224aa", + "sha256:a8031074a397a5925d06b590121f8339d34a5a74cfe6970f8a1124eb8b83f4ac", + "sha256:aab9e522efff3993a9e98ab14263d4e20211e62da088298089a03056980a3e69", + "sha256:ae579143826c6f05a361d9546446c432a165ecf1c0b720bbfd81152645cb897d", + "sha256:ae90b9e50fe1bd115b24785e962b51130340408156d34d67b5f8f3fa6540938e", + "sha256:b18cf68255a476b927910c6873d9ed00da692bb293c5b10b282bd48a0afe3ae2", + "sha256:b7efb12e5071ad8d5b547487bdad489fbd4a5a35a0fc36a1941517a6ad7f23e0", + "sha256:c4d9f15ffe68bcd3898b0ad7233af01b15c57d91cd1667f8d868e0eacbfe3f87", + "sha256:c53100c8ee5a1e102766abde2158077d8c374bee0639201f11d3032e3555dfbc", + "sha256:c57e493a0faea1e4c38f860d6862ba6832723396c884fbf938ff5e9b224200e2", + "sha256:c8319e0bd6a7b45ad76166cc3d5d6a36c97d0c82a196f478c3ee5346566eebfd", + "sha256:caffda619099cfd4f63d48462f6aadbecee3ad9603b4b88b60cb821c1b258576", + "sha256:cc0c316fba3ce72ac3ab7902a888b9dc4979162d320823679da270c2d9ad0cad", + "sha256:cdd02a08205dc90238669f082747612cb3c82bd2c717adc60f9b9ecadb540f80", + "sha256:d50ac34835c6a4a0d456b5db559b82047403c4317b3bc73b3455fefdbdc54b0a", + "sha256:d6b9dd6aa03c812017411734e496c44fef29b43dba1e3dd1fa7361bbacfc1354", + "sha256:da3131ef2b940b99106f29dfbc30d9505643f766704e14c5d5e504e6a480c35e", + "sha256:da43cbe593e3c87d07108d0ebd73771dc414488f1f91ed2e204b0370b94b37ac", + "sha256:dd59638025160056687d598b054b64a79183f8065eae0d3f5ca523cde9943940", + "sha256:e1895e949f8849bc2757c0dbac28422a04be031204df46a56ab34bcf98507342", + "sha256:e1a79ad49f346aa1a2921f31e8dbbab4d64484823e813a002679eaa46cba39e1", + "sha256:e460475719721d59cd54a350c1f71c797c763212c836bf48585478c5514d2854", + "sha256:e64ffaf8f6e17ca15eb48344d86a7a741454526f3a3fa56bc493ad9d7ec63936", + "sha256:e6e3ccebdbd6e53474b0bb7ab8b88e83c0cfe91484b25e058e581348ee5a01a5", + "sha256:e758d271ed0286d146cf7c04c539a5169a888dd0b57026be621547e756af55bc", + "sha256:f087879f1ffde024dd2788a30d55acd67959dcf6c431e9d3682d1c491a0eb474", + "sha256:f477d26183e94eaafc60b983ab25af2a809a1b48ce4debb57b343f671b7a90b6", + "sha256:fc535cb898ef88333cf317777ecdfe0faac1c2a3187ef7eb061b6f7ecf7e6bae" ], "markers": "python_version >= '3.8'", - "version": "==2.27.1" + "version": "==2.23.2" }, "pydantic-settings": { "hashes": [ - "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", - "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0" + "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", + "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd" ], "markers": "python_version >= '3.8'", - "version": "==2.6.1" + "version": "==2.7.1" }, "pynacl": { "hashes": [ @@ -1228,11 +1197,49 @@ }, "pyparsing": { "hashes": [ - "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", - "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" + "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", + "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a" + ], + "markers": "python_version > '3.0'", + "version": "==3.2.1" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" ], - "markers": "python_version >= '3.9'", - "version": "==3.2.0" + "markers": "python_version >= '3.8'", + "version": "==0.20.0" }, "pyserial": { "hashes": [ @@ -1280,14 +1287,6 @@ "markers": "python_full_version >= '3.6.0'", "version": "==1.2.1" }, - "referencing": { - "hashes": [ - "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", - "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" - ], - "markers": "python_version >= '3.8'", - "version": "==0.35.1" - }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", @@ -1304,115 +1303,6 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, - "rpds-py": { - "hashes": [ - "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", - "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", - "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", - "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", - "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", - "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543", - "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2", - "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a", - "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", - "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", - "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d", - "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", - "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", - "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", - "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99", - "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", - "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", - "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", - "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", - "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", - "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f", - "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3", - "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca", - "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d", - "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e", - "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", - "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea", - "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", - "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", - "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", - "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", - "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723", - "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e", - "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", - "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6", - "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", - "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091", - "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", - "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", - "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1", - "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728", - "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", - "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", - "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", - "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7", - "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a", - "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", - "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", - "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", - "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", - "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055", - "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d", - "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0", - "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", - "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", - "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c", - "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", - "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", - "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", - "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", - "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11", - "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", - "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", - "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64", - "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b", - "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", - "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c", - "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9", - "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", - "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb", - "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", - "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", - "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", - "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c", - "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", - "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", - "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f", - "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", - "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", - "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", - "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", - "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74", - "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", - "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", - "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", - "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648", - "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", - "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3", - "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", - "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520", - "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831", - "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", - "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", - "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", - "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", - "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", - "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", - "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", - "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", - "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5", - "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d", - "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", - "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e" - ], - "markers": "python_version >= '3.9'", - "version": "==0.22.3" - }, "rsa": { "hashes": [ "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", @@ -1423,11 +1313,11 @@ }, "setuptools": { "hashes": [ - "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", - "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d" + "sha256:84fb203f278ebcf5cd08f97d3fb96d3fbed4b629d500b29ad60d11e00769b183", + "sha256:886ff7b16cd342f1d1defc16fc98c9ce3fde69e087a4e1983d7ab634e5f41f4f" ], "markers": "python_version >= '3.9'", - "version": "==75.6.0" + "version": "==75.7.0" }, "six": { "hashes": [ @@ -1439,12 +1329,12 @@ }, "slack-sdk": { "hashes": [ - "sha256:a5e74c00c99dc844ad93e501ab764a20d86fa8184bbc9432af217496f632c4ee", - "sha256:b8cccadfa3d4005a5e6529f52000d25c583f46173fda8e9136fdd2bc58923ff6" + "sha256:c61f57f310d85be83466db5a98ab6ae3bb2e5587437b54fa0daa8fae6a0feffa", + "sha256:ff61db7012160eed742285ea91f11c72b7a38a6500a7f6c5335662b4bc6b853d" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.33.5" + "version": "==3.34.0" }, "slackclient": { "hashes": [ @@ -1472,27 +1362,27 @@ }, "types-httplib2": { "hashes": [ - "sha256:1eda99fea18ec8a1dc1a725ead35b889d0836fec1b11ae6f1fe05440724c1d15", - "sha256:8cd706fc81f0da32789a4373a28df6f39e9d5657d1281db4d2fd22ee29e83661" + "sha256:42b67f16a6b0abb337a1fcea628dcd335e1e75f32cd198a657f41a6f4d507508", + "sha256:b15aed53ae5430b87205b6ac270d6332cb5e28e27151e2ac4848fe417827eb54" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.22.0.20240310" + "version": "==0.22.0.20241221" }, "types-pytz": { "hashes": [ - "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7", - "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44" + "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9", + "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5" ], "markers": "python_version >= '3.8'", - "version": "==2024.2.0.20241003" + "version": "==2024.2.0.20241221" }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.13'", "version": "==4.12.2" }, "tzdata": { @@ -1500,7 +1390,7 @@ "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" ], - "markers": "python_version >= '2'", + "markers": "python_version >= '3.9'", "version": "==2024.2" }, "uritemplate": { @@ -1513,11 +1403,11 @@ }, "urllib3": { "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '3.8'", - "version": "==2.2.3" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" }, "wrapt": { "hashes": [ @@ -1689,11 +1579,11 @@ }, "attrs": { "hashes": [ - "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", - "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" + "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", + "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308" ], - "markers": "python_version >= '3.7'", - "version": "==24.2.0" + "markers": "python_version >= '3.8'", + "version": "==24.3.0" }, "black": { "hashes": [ @@ -1735,130 +1625,117 @@ }, "certifi": { "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", + "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" ], "markers": "python_version >= '3.6'", - "version": "==2024.8.30" + "version": "==2024.12.14" }, "charset-normalizer": { "hashes": [ - "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", - "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", - "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", - "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", - "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", - "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", - "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", - "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", - "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", - "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", - "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", - "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", - "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", - "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", - "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", - "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", - "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", - "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", - "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", - "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", - "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", - "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", - "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", - "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", - "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", - "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", - "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", - "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", - "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", - "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", - "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", - "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", - "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", - "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", - "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", - "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", - "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", - "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", - "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", - "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", - "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", - "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", - "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", - "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", - "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", - "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", - "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", - "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", - "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", - "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", - "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", - "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", - "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", - "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", - "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", - "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", - "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", - "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", - "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", - "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", - "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", - "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", - "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", - "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", - "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", - "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", - "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", - "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", - "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", - "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", - "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", - "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", - "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", - "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", - "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", - "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", - "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", - "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", - "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", - "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", - "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", - "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", - "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", - "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", - "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", - "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", - "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", - "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", - "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", - "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", - "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", - "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", - "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", - "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", - "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", - "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", - "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", - "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", - "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", - "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", - "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", - "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", - "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", - "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", - "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.4.0" + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "version": "==8.1.8" }, "colorama": { "hashes": [ @@ -1871,71 +1748,71 @@ }, "coverage": { "hashes": [ - "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", - "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", - "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", - "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", - "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", - "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", - "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", - "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", - "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", - "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717", - "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", - "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198", - "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", - "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3", - "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", - "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", - "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08", - "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf", - "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", - "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710", - "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", - "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", - "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", - "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", - "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb", - "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", - "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", - "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", - "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6", - "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", - "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", - "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa", - "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", - "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", - "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", - "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", - "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", - "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678", - "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", - "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902", - "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", - "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845", - "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", - "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464", - "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be", - "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", - "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", - "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", - "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1", - "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", - "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5", - "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073", - "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4", - "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", - "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", - "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", - "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599", - "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", - "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b", - "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec", - "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", - "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3" + "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", + "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f", + "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", + "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", + "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", + "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", + "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", + "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", + "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", + "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", + "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", + "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", + "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", + "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", + "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", + "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59", + "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", + "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18", + "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", + "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", + "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", + "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", + "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", + "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", + "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90", + "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", + "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a", + "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", + "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", + "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", + "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", + "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", + "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", + "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d", + "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", + "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", + "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", + "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", + "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", + "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", + "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", + "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", + "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", + "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4", + "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25", + "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", + "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", + "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", + "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", + "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315", + "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", + "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", + "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27", + "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", + "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", + "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", + "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", + "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", + "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", + "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", + "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", + "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f" ], "markers": "python_version >= '3.9'", - "version": "==7.6.9" + "version": "==7.6.10" }, "flake8": { "hashes": [ @@ -1982,12 +1859,12 @@ }, "google-api-python-client": { "hashes": [ - "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17", - "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad" + "sha256:0b0231db106324c659bf8b85f390391c00da57a60ebc4271e33def7aac198c75", + "sha256:2ee342d0967ad1cedec43ccd7699671d94bff151e1f06833ea81303f9a6d86fd" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.154.0" + "version": "==2.157.0" }, "google-api-python-client-stubs": { "hashes": [ @@ -2000,11 +1877,11 @@ }, "google-auth": { "hashes": [ - "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", - "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1" + "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00", + "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0" ], "markers": "python_version >= '3.7'", - "version": "==2.36.0" + "version": "==2.37.0" }, "google-auth-httplib2": { "hashes": [ @@ -2137,20 +2014,20 @@ }, "protobuf": { "hashes": [ - "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c", - "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331", - "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", - "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", - "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", - "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853", - "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57", - "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", - "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", - "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", - "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18" + "sha256:13d6d617a2a9e0e82a88113d7191a1baa1e42c2cc6f5f1398d3b054c8e7e714a", + "sha256:2d2e674c58a06311c8e99e74be43e7f3a8d1e2b2fdf845eaa347fbd866f23355", + "sha256:36000f97ea1e76e8398a3f02936aac2a5d2b111aae9920ec1b769fc4a222c4d9", + "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e", + "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9", + "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb", + "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e", + "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e", + "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851", + "sha256:e621a98c0201a7c8afe89d9646859859be97cb22b8bf1d8eacfd90d5bda2eb19", + "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181" ], "markers": "python_version >= '3.8'", - "version": "==5.29.1" + "version": "==5.29.2" }, "py": { "hashes": [ @@ -2202,11 +2079,11 @@ }, "pyparsing": { "hashes": [ - "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", - "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" + "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", + "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a" ], - "markers": "python_version >= '3.9'", - "version": "==3.2.0" + "markers": "python_version > '3.0'", + "version": "==3.2.1" }, "pytest": { "hashes": [ @@ -2289,12 +2166,12 @@ }, "types-httplib2": { "hashes": [ - "sha256:1eda99fea18ec8a1dc1a725ead35b889d0836fec1b11ae6f1fe05440724c1d15", - "sha256:8cd706fc81f0da32789a4373a28df6f39e9d5657d1281db4d2fd22ee29e83661" + "sha256:42b67f16a6b0abb337a1fcea628dcd335e1e75f32cd198a657f41a6f4d507508", + "sha256:b15aed53ae5430b87205b6ac270d6332cb5e28e27151e2ac4848fe417827eb54" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.22.0.20240310" + "version": "==0.22.0.20241221" }, "types-requests": { "hashes": [ @@ -2309,7 +2186,7 @@ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.13'", "version": "==4.12.2" }, "uritemplate": { @@ -2322,11 +2199,11 @@ }, "urllib3": { "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '3.8'", - "version": "==2.2.3" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" } } } diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index c2dadaae54c..ec1dfdcadf7 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -24,10 +24,8 @@ def retrieve_protocol_file( ) -> Path | str: """Find and copy protocol file on robot with error.""" protocol_dir = f"/var/lib/opentrons-robot-server/7.1/protocols/{protocol_id}" - - print(f"FILE TO FIND: {protocol_dir}/{protocol_id}") - # Copy protocol file found in robot oto host computer - save_dir = Path(f"{storage}/protocol_errors") + # Copy protocol file found in robot onto host computer + save_dir = Path(f"{storage}") command = ["scp", "-r", f"root@{robot_ip}:{protocol_dir}", save_dir] try: # If file found and copied return path to file @@ -62,7 +60,6 @@ def compare_current_trh_to_average( # Find average conditions of errored time period df_all_trh = pd.DataFrame(all_trh_data) # Convert timestamps to datetime objects - print(f'TIMESTAMP: {df_all_trh["Timestamp"]}') try: df_all_trh["Timestamp"] = pd.to_datetime( df_all_trh["Timestamp"], format="mixed", utc=True @@ -196,7 +193,6 @@ def read_each_log(folder_path: str, issue_url: str) -> None: for file_name in os.listdir(folder_path): file_path = os.path.join(folder_path, file_name) not_found_words = [] - print(file_path) if file_path.endswith(".log"): with open(file_path) as file: lines = file.readlines() @@ -341,11 +337,8 @@ def get_robot_state( if "8.2" in affects_version: labels.append("8_2_0") parent = affects_version + " Bugs" - print(components) end_time = datetime.now() - print(end_time) start_time = end_time - timedelta(hours=2) - print(start_time) # Get current temp/rh compared to historical data temp_rh_string = compare_current_trh_to_average( parent, start_time, end_time, "", storage_directory @@ -554,11 +547,17 @@ def get_run_error_info_from_robot( sys.exit() if len(run_or_other) < 1: # Retrieve the most recently run protocol file - protocol_files_path = retrieve_protocol_file( + protocol_folder = retrieve_protocol_file( protocol_ids[-1], ip, storage_directory ) + protocol_folder_path = os.path.join(protocol_folder, protocol_ids[-1]) + # Path to protocol folder + list_of_files = os.listdir(protocol_folder_path) + for file in list_of_files: + if str(file).endswith(".py"): + protocol_file_path = os.path.join(protocol_folder_path, file) # Set protocol_found to true if python protocol was successfully copied over - if protocol_files_path: + if protocol_file_path: protocol_found = True one_run = error_runs[-1] # Most recent run with error. @@ -612,15 +611,13 @@ def get_run_error_info_from_robot( # OPEN TICKET issue_url = ticket.open_issue(issue_key) # MOVE FILES TO ERROR FOLDER. - print(protocol_files_path) - error_files = [saved_file_path_calibration, run_log_file_path] + file_paths - - # Move protocol file(s) to error folder - if protocol_files_path: - for file in os.listdir(protocol_files_path): - error_files.append(os.path.join(protocol_files_path, file)) + error_files = [ + saved_file_path_calibration, + run_log_file_path, + protocol_file_path, + ] + file_paths - error_folder_path = os.path.join(storage_directory, "issue_key") + error_folder_path = os.path.join(storage_directory, issue_key) os.makedirs(error_folder_path, exist_ok=True) for source_file in error_files: try: diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index d4570d20110..f8d2b028525 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -638,6 +638,11 @@ def get_error_info(file_results: Dict[str, Any]) -> Dict[str, Any]: recoverable_errors.get(error_type, 0) + 1 ) # Get run-ending error info + module_dict = { + "heatershaker": "heaterShakerModuleV1", + "thermocycler": "thermocyclerModuleV2", + "temperature module": "temperatureModuleV2", + } try: run_command_error = commands_of_run[-1]["error"] error_type = run_command_error.get("errorType", "") @@ -647,6 +652,17 @@ def get_error_info(file_results: Dict[str, Any]) -> Dict[str, Any]: error_instrument = run_command_error.get("errorInfo", {}).get( "node", run_command_error.get("errorInfo", {}).get("port", "") ) + if "gripper" in error_instrument: + # get gripper serial number + error_instrument = file_results["extension"] + else: + # get module serial number + for module in module_dict.keys(): + if module in error_instrument: + for module_list in file_results["modules"]: + model = module_list["model"] + if model == module_dict[module]: + error_instrument = module_list["serialNumber"] except (IndexError, KeyError): try: error_details = file_results.get("errors", [{}])[0] diff --git a/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py index 894f80dcdea..a3fd6251a43 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py +++ b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py @@ -33,7 +33,7 @@ def add_parameters(parameters: ParameterContext) -> None: parameters.add_int( variable_name="number_of_runs", display_name="Number of Runs", - default=2, + default=4, minimum=1, maximum=4, ) @@ -53,7 +53,7 @@ def run(protocol: ProtocolContext) -> None: bind_vol = 300.0 sample_vol = 180.0 elution_vol = 100.0 - helpers.comment_protocol_version(protocol, "01") + helpers.comment_protocol_version(protocol, "02") # Same for all HDQ Extractions deepwell_type = "nest_96_wellplate_2ml_deep" if not dry_run: @@ -87,7 +87,7 @@ def run(protocol: ProtocolContext) -> None: magblock: MagneticBlockContext = protocol.load_module( "magneticBlockV1", "C1" ) # type: ignore[assignment] - liquid_waste = protocol.load_labware("nest_1_reservoir_195ml", "B3", "Liquid Waste") + liquid_waste = protocol.load_labware("nest_1_reservoir_290ml", "B3", "Liquid Waste") waste = liquid_waste.wells()[0].top() lysis_reservoir = protocol.load_labware(deepwell_type, "D2", "Lysis reservoir") @@ -417,7 +417,6 @@ def clean() -> None: elutionplate, wash2_reservoir, wash1_reservoir, - liquid_waste, ] helpers.clean_up_plates(pip, plates_to_clean, liquid_waste["A1"], 1000) diff --git a/analyses-snapshot-testing/README.md b/analyses-snapshot-testing/README.md index 03ce1d87518..e38df61b4d5 100644 --- a/analyses-snapshot-testing/README.md +++ b/analyses-snapshot-testing/README.md @@ -86,3 +86,20 @@ You have the option to specify one or many protocols to run the analyses on. Thi ### Updating the snapshots locally - `make snapshot-test-update-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created, updating the snapshots by passing the `--update-snapshots` flag to the test + +### Add some protocols to the analyses battery + +> The below instructions avoid needing docker and executing snapshot tests locally. + +1. create new protocol file(s) in the [files/protocols](./files/protocols) directory following the naming convention in [files/README.md](./files/README.md) +1. add the protocol(s) to the [protocols.py](./automation/data/protocols.py) +1. `make format` (make sure you have followed setup instructions) +1. commit and push your branch +1. open a PR and add the label `gen-analyses-snapshot-pr` +1. when the snapshot fails because your new protocols don't have snapshots a PR will be created that heals. +1. merge the healing PR if the snapshots are as expected +1. get a review and merge! 🎉 now your protocols are a part of the test + +### Add a protocol with overrides to the analyses battery + +> TODO when we have a more straight forward example diff --git a/analyses-snapshot-testing/automation/data/protocols.py b/analyses-snapshot-testing/automation/data/protocols.py index ada74a736e0..8e19251d49e 100644 --- a/analyses-snapshot-testing/automation/data/protocols.py +++ b/analyses-snapshot-testing/automation/data/protocols.py @@ -708,6 +708,48 @@ class Protocols: file_extension="py", robot="Flex", ) + # analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_trash.py + Flex_X_v2_21_plate_reader_no_trash: Protocol = Protocol( + file_stem="Flex_X_v2_21_plate_reader_no_trash", + file_extension="py", + robot="Flex", + ) + # analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate.py + Flex_X_v2_21_plate_reader_wrong_plate: Protocol = Protocol( + file_stem="Flex_X_v2_21_plate_reader_wrong_plate", + file_extension="py", + robot="Flex", + ) + # analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate2.py + Flex_X_v2_21_plate_reader_wrong_plate2: Protocol = Protocol( + file_stem="Flex_X_v2_21_plate_reader_wrong_plate2", + file_extension="py", + robot="Flex", + ) + # analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_bad_slot.py + Flex_X_v2_21_plate_reader_bad_slot: Protocol = Protocol( + file_stem="Flex_X_v2_21_plate_reader_bad_slot", + file_extension="py", + robot="Flex", + ) + # analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_close_lid.py + Flex_X_v2_21_plate_reader_no_close_lid: Protocol = Protocol( + file_stem="Flex_X_v2_21_plate_reader_no_close_lid", + file_extension="py", + robot="Flex", + ) + # analyses-snapshot-testing/files/protocols/Flex_S_v2_21_tc_lids_happy_path.py + Flex_S_v2_21_tc_lids_happy_path: Protocol = Protocol( + file_stem="Flex_S_v2_21_tc_lids_happy_path", + file_extension="py", + robot="Flex", + ) + # analyses-snapshot-testing/files/protocols/Flex_X_v2_21_tc_lids_wrong_target.py + Flex_X_v2_21_tc_lids_wrong_target: Protocol = Protocol( + file_stem="Flex_X_v2_21_tc_lids_wrong_target", + file_extension="py", + robot="Flex", + ) OT2_X_v2_18_None_None_duplicateRTPVariableName: Protocol = Protocol( file_stem="OT2_X_v2_18_None_None_duplicateRTPVariableName", diff --git a/analyses-snapshot-testing/files/protocols/README.md b/analyses-snapshot-testing/files/README.md similarity index 100% rename from analyses-snapshot-testing/files/protocols/README.md rename to analyses-snapshot-testing/files/README.md diff --git a/analyses-snapshot-testing/files/protocols/Flex_S_v2_21_tc_lids_happy_path.py b/analyses-snapshot-testing/files/protocols/Flex_S_v2_21_tc_lids_happy_path.py new file mode 100644 index 00000000000..41adb0c122d --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_S_v2_21_tc_lids_happy_path.py @@ -0,0 +1,99 @@ +from typing import List, Dict, Any, Optional +from opentrons.protocol_api import ProtocolContext, Labware + +metadata = {"protocolName": "Opentrons Flex Deck Riser with TC Lids Test"} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +""" +Setup: + - 1-5x lids are stacked in deck D2 + - Thermocycler installed + +Run: + - For each lid in the stack (1-5x) + - Move lid in D2 to Thermocycler + - Remove top-most lid + - PAUSE, wait for tester to press continue + - Move lid from Thermocycler to new slot C2 + - Stacked onto any previously placed lids +""" + +LID_STARTING_SLOT = "B2" +LID_ENDING_SLOT = "C2" +LID_COUNT = 3 +LID_DEFINITION = "opentrons_tough_pcr_auto_sealing_lid" +LID_BOTTOM_DEFINITION = "opentrons_tough_pcr_auto_sealing_lid" +DECK_RISER_NAME = "opentrons_flex_deck_riser" +USING_THERMOCYCLER = True + +OFFSET_DECK = { + "pick-up": {"x": 0, "y": 0, "z": 0}, + "drop": {"x": 0, "y": 0, "z": 0}, +} +OFFSET_THERMOCYCLER = { + "pick-up": {"x": 0, "y": 0, "z": 0}, + "drop": {"x": 0, "y": 0, "z": 0}, +} + + +def _move_labware_with_offset_and_pause( + protocol: ProtocolContext, + labware: Labware, + destination: Any, + pick_up_offset: Optional[Dict[str, float]] = None, + drop_offset: Optional[Dict[str, float]] = None, +) -> None: + protocol.move_labware( + labware, + destination, + use_gripper=True, + pick_up_offset=pick_up_offset, + drop_offset=drop_offset, + ) + + +def run(protocol: ProtocolContext): + # SETUP + deck_riser_adapter = protocol.load_adapter(DECK_RISER_NAME, "B2") + + lids: List[Labware] = [deck_riser_adapter.load_labware(LID_BOTTOM_DEFINITION)] + for i in range(LID_COUNT - 1): + lids.append(lids[-1].load_labware(LID_DEFINITION)) + lids.reverse() # NOTE: reversing to more easily loop through lids from top-to-bottom + if USING_THERMOCYCLER: + # TODO: confirm if we need to load 96-well adapter onto Thermocycler + thermocycler = protocol.load_module("thermocyclerModuleV2") + thermocycler.open_lid() + plate_in_cycler = thermocycler.load_labware("armadillo_96_wellplate_200ul_pcr_full_skirt") + else: + plate_in_cycler = None + + # RUN + prev_moved_lid: Optional[Labware] = None + for lid in lids: + + if USING_THERMOCYCLER: + _move_labware_with_offset_and_pause( + protocol, + lid, + plate_in_cycler, + pick_up_offset=OFFSET_DECK["pick-up"], + drop_offset=OFFSET_THERMOCYCLER["drop"], + ) + _move_labware_with_offset_and_pause( + protocol, + lid, + prev_moved_lid if prev_moved_lid else LID_ENDING_SLOT, + pick_up_offset=OFFSET_THERMOCYCLER["pick-up"], + drop_offset=OFFSET_DECK["drop"], + ) + else: + _move_labware_with_offset_and_pause( + protocol, + lid, + prev_moved_lid if prev_moved_lid else LID_ENDING_SLOT, + pick_up_offset=OFFSET_DECK["pick-up"], + drop_offset=OFFSET_DECK["drop"], + ) + prev_moved_lid = lid diff --git a/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_bad_slot.py b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_bad_slot.py new file mode 100644 index 00000000000..ad6d96be786 --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_bad_slot.py @@ -0,0 +1,22 @@ +from typing import cast +from opentrons import protocol_api +from opentrons.protocol_api.module_contexts import AbsorbanceReaderContext + +from opentrons import protocol_api +from opentrons.protocol_api import SINGLE, ALL + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} +metadata = {"protocolName": "plate_reader bad slot"} + + +def run(protocol: protocol_api.ProtocolContext): + partial_rack = protocol.load_labware(load_name="opentrons_flex_96_tiprack_1000ul", location="D3") + trash = protocol.load_trash_bin("A3") + instrument = protocol.load_instrument(instrument_name="flex_8channel_1000", mount="right") + instrument.configure_nozzle_layout(style=SINGLE, start="H1", tip_racks=[partial_rack]) + + plate_1 = protocol.load_labware("nest_96_wellplate_200ul_flat", "D1") + mod = protocol.load_module("absorbanceReaderV1", "C1") + mod.open_lid() + protocol.move_labware(plate_1, mod, use_gripper=True) + mod.close_lid() diff --git a/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_close_lid.py b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_close_lid.py new file mode 100644 index 00000000000..e231b4d0e1f --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_close_lid.py @@ -0,0 +1,88 @@ +from typing import cast +from opentrons import protocol_api +from opentrons.protocol_api.module_contexts import AbsorbanceReaderContext + +# metadata +metadata = { + "protocolName": "Absorbance Reader no close lid", + "author": "Platform Expansion", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +# protocol run function +def run(protocol: protocol_api.ProtocolContext): + mod = cast(AbsorbanceReaderContext, protocol.load_module("absorbanceReaderV1", "D3")) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "C2") + tiprack_1000 = protocol.load_labware(load_name="opentrons_flex_96_tiprack_50ul", location="B2") + trash_labware = protocol.load_trash_bin("B3") + instrument = protocol.load_instrument("flex_8channel_50", "right", tip_racks=[tiprack_1000]) + + # pick up tip and perform action + instrument.pick_up_tip(tiprack_1000.wells_by_name()["A1"]) + instrument.aspirate(30, plate.wells_by_name()["A1"]) + instrument.dispense(30, plate.wells_by_name()["B1"]) + instrument.return_tip() + + # Initialize to a single wavelength with reference wavelength + # Issue: Make sure there is no labware here or youll get an error + # mod.close_lid() + mod.initialize("single", [600], 450) + + # NOTE: CANNOT INITIALIZE WITH THE LID OPEN + + # Remove the Plate Reader lid using the Gripper. + mod.open_lid() + protocol.move_labware(plate, mod, use_gripper=True) + mod.close_lid() + + # Take a reading and show the resulting absorbance values. + # Issue: cant read before you initialize or you an get an error + result = mod.read() + msg = f"single: {result}" + protocol.comment(msg=msg) + protocol.pause(msg=msg) + + # Initialize to multiple wavelengths + protocol.pause(msg="Perform Multi Read") + mod.open_lid() + protocol.move_labware(plate, "C2", use_gripper=True) + + mod.close_lid() + + # mod.initialize('multi', [450, 570, 600]) + mod.initialize("multi", [450, 600]) + # Open the lid and move the labware into the reader + mod.open_lid() + protocol.move_labware(plate, mod, use_gripper=True) + + # pick up tip and perform action on labware inside plate reader + instrument.pick_up_tip(tiprack_1000.wells_by_name()["A1"]) + instrument.aspirate(30, plate.wells_by_name()["A1"]) + instrument.dispense(30, plate.wells_by_name()["B1"]) + instrument.return_tip() + + mod.close_lid() + + # Take reading + result = mod.read() + msg = f"multi: {result}" + protocol.comment(msg=msg) + protocol.pause(msg=msg) + + # Take a reading and save to csv + protocol.pause(msg="Perform Read and Save to CSV") + result = mod.read(export_filename="plate_reader_csv.csv") + msg = f"csv: {result}" + protocol.pause(msg=msg) + + # Place the Plate Reader lid back on using the Gripper. + mod.open_lid() + protocol.move_labware(plate, "C2", use_gripper=True) + mod.close_lid() + + mod.read(export_filename="csv_name.csv") diff --git a/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_trash.py b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_trash.py new file mode 100644 index 00000000000..8c839a54ff9 --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_no_trash.py @@ -0,0 +1,24 @@ +from typing import cast +from opentrons import protocol_api +from opentrons.protocol_api.module_contexts import AbsorbanceReaderContext + +from opentrons import protocol_api +from opentrons.protocol_api import SINGLE, ALL + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} +metadata = {"protocolName": "plate_reader no trash"} + + +def run(protocol: protocol_api.ProtocolContext): + partial_rack = protocol.load_labware(load_name="opentrons_flex_96_tiprack_1000ul", location="D2") + trash = protocol.load_trash_bin("A3") + instrument = protocol.load_instrument(instrument_name="flex_8channel_1000", mount="right") + instrument.configure_nozzle_layout(style=SINGLE, start="H1", tip_racks=[partial_rack]) + + plate_1 = protocol.load_labware("nest_96_wellplate_200ul_flat", "C1") + + mod = protocol.load_module("absorbanceReaderV1", "B3") + + mod.open_lid() + protocol.move_labware(plate_1, mod, use_gripper=True) + protocol.move_labware(plate_1, trash, use_gripper=True) diff --git a/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate.py b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate.py new file mode 100644 index 00000000000..bb3811a1631 --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate.py @@ -0,0 +1,24 @@ +from typing import cast +from opentrons import protocol_api +from opentrons.protocol_api.module_contexts import AbsorbanceReaderContext + +from opentrons import protocol_api +from opentrons.protocol_api import SINGLE, ALL + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} +metadata = {"protocolName": "plate_reader wrong plate"} + + +def run(protocol: protocol_api.ProtocolContext): + partial_rack = protocol.load_labware(load_name="opentrons_flex_96_tiprack_1000ul", location="D2") + trash = protocol.load_trash_bin("A3") + instrument = protocol.load_instrument(instrument_name="flex_8channel_1000", mount="right") + instrument.configure_nozzle_layout(style=SINGLE, start="H1", tip_racks=[partial_rack]) + + plate_1 = protocol.load_labware("corning_12_wellplate_6.9ml_flat", "C2") + + mod = protocol.load_module("absorbanceReaderV1", "B3") + + mod.open_lid() + protocol.move_labware(plate_1, mod, use_gripper=True) + protocol.move_labware(plate_1, trash, use_gripper=True) diff --git a/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate2.py b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate2.py new file mode 100644 index 00000000000..cf6d0aefee9 --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_plate_reader_wrong_plate2.py @@ -0,0 +1,24 @@ +from typing import cast +from opentrons import protocol_api +from opentrons.protocol_api.module_contexts import AbsorbanceReaderContext + +from opentrons import protocol_api +from opentrons.protocol_api import SINGLE, ALL + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} +metadata = {"protocolName": "plate_reader wrong plate"} + + +def run(protocol: protocol_api.ProtocolContext): + partial_rack = protocol.load_labware(load_name="opentrons_flex_96_tiprack_1000ul", location="D2") + trash = protocol.load_trash_bin("A3") + instrument = protocol.load_instrument(instrument_name="flex_8channel_1000", mount="right") + instrument.configure_nozzle_layout(style=SINGLE, start="H1", tip_racks=[partial_rack]) + + plate_1 = protocol.load_labware("thermoscientificnunc_96_wellplate_2000ul", "C2") + + mod = protocol.load_module("absorbanceReaderV1", "B3") + + mod.open_lid() + protocol.move_labware(plate_1, mod, use_gripper=True) + protocol.move_labware(plate_1, trash, use_gripper=True) diff --git a/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_tc_lids_wrong_target.py b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_tc_lids_wrong_target.py new file mode 100644 index 00000000000..bf6ca7f7c89 --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_X_v2_21_tc_lids_wrong_target.py @@ -0,0 +1,26 @@ +from typing import List, Dict, Any, Optional +from opentrons.protocol_api import ProtocolContext, Labware + +metadata = {"Stacking a deep well plate on its adapter "} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def run(protocol: ProtocolContext): + temp_mod = protocol.load_module(module_name="temperature module gen2", location="D1") + LID_COUNT = 5 + LID_DEFINITION = "opentrons_tough_pcr_auto_sealing_lid" + LID_BOTTOM_DEFINITION = "opentrons_tough_pcr_auto_sealing_lid" + adapter = temp_mod.load_adapter("opentrons_96_deep_well_temp_mod_adapter") + stack = protocol.load_labware("nest_96_wellplate_2ml_deep", "A1") + deck_riser_adapter = protocol.load_adapter("opentrons_flex_deck_riser", "B2") + protocol.move_labware(stack, adapter) + lids = [deck_riser_adapter.load_labware(LID_BOTTOM_DEFINITION, "D2")] + for i in range(LID_COUNT - 1): + lids.append(lids[-1].load_labware(LID_DEFINITION)) + lids.reverse() # NOTE: reversing to more easily loop through lids from top-to-bottom + + protocol.move_labware( + lids[0], + stack, + use_gripper=True, + ) diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[34f32336bc][Flex_X_v2_21_plate_reader_wrong_plate2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[34f32336bc][Flex_X_v2_21_plate_reader_wrong_plate2].json new file mode 100644 index 00000000000..7f50cfdd654 --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[34f32336bc][Flex_X_v2_21_plate_reader_wrong_plate2].json @@ -0,0 +1,2558 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "73d9d4d55ae8466f3a793ceb70545fa5", + "notes": [], + "params": { + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16.0, + "gripHeightFromLabwareBottom": 23.9, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "quirks": [], + "tipLength": 95.6, + "tipOverlap": 10.5 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + }, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "bd403a1c851a75b4b68ce34796d713fa", + "notes": [], + "params": { + "liquidPresenceDetection": false, + "mount": "right", + "pipetteName": "p1000_multi_flex", + "tipOverlapNotAfterVersion": "v3" + }, + "result": { + "pipetteId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "configureNozzleLayout", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "2c37ad797da7df791b57a7843a203e88", + "notes": [], + "params": { + "configurationParams": { + "primaryNozzle": "H1", + "style": "SINGLE" + }, + "pipetteId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "691afd54dfa7982fb89e5f77c763bfd4", + "notes": [], + "params": { + "loadName": "thermoscientificnunc_96_wellplate_2000ul", + "location": { + "slotName": "C2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Thermo Scientific", + "brandId": [ + "278743", + "278752" + ], + "links": [ + "https://www.thermofisher.com/order/catalog/product/278743" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.8, + "yDimension": 85.5, + "zDimension": 43.6 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "u" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "Thermo Scientific Nunc 96 Well Plate 2000 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "thermoscientificnunc_96_wellplate_2000ul", + "magneticModuleEngageHeight": 6, + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 74.2, + "z": 2.1 + }, + "A10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 74.2, + "z": 2.1 + }, + "A11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 74.2, + "z": 2.1 + }, + "A12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 74.2, + "z": 2.1 + }, + "A2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 74.2, + "z": 2.1 + }, + "A3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 74.2, + "z": 2.1 + }, + "A4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 74.2, + "z": 2.1 + }, + "A5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 74.2, + "z": 2.1 + }, + "A6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 74.2, + "z": 2.1 + }, + "A7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 74.2, + "z": 2.1 + }, + "A8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 74.2, + "z": 2.1 + }, + "A9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 74.2, + "z": 2.1 + }, + "B1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 65.2, + "z": 2.1 + }, + "B10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 65.2, + "z": 2.1 + }, + "B11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 65.2, + "z": 2.1 + }, + "B12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 65.2, + "z": 2.1 + }, + "B2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 65.2, + "z": 2.1 + }, + "B3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 65.2, + "z": 2.1 + }, + "B4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 65.2, + "z": 2.1 + }, + "B5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 65.2, + "z": 2.1 + }, + "B6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 65.2, + "z": 2.1 + }, + "B7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 65.2, + "z": 2.1 + }, + "B8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 65.2, + "z": 2.1 + }, + "B9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 65.2, + "z": 2.1 + }, + "C1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 56.2, + "z": 2.1 + }, + "C10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 56.2, + "z": 2.1 + }, + "C11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 56.2, + "z": 2.1 + }, + "C12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 56.2, + "z": 2.1 + }, + "C2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 56.2, + "z": 2.1 + }, + "C3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 56.2, + "z": 2.1 + }, + "C4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 56.2, + "z": 2.1 + }, + "C5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 56.2, + "z": 2.1 + }, + "C6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 56.2, + "z": 2.1 + }, + "C7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 56.2, + "z": 2.1 + }, + "C8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 56.2, + "z": 2.1 + }, + "C9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 56.2, + "z": 2.1 + }, + "D1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 47.2, + "z": 2.1 + }, + "D10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 47.2, + "z": 2.1 + }, + "D11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 47.2, + "z": 2.1 + }, + "D12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 47.2, + "z": 2.1 + }, + "D2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 47.2, + "z": 2.1 + }, + "D3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 47.2, + "z": 2.1 + }, + "D4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 47.2, + "z": 2.1 + }, + "D5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 47.2, + "z": 2.1 + }, + "D6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 47.2, + "z": 2.1 + }, + "D7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 47.2, + "z": 2.1 + }, + "D8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 47.2, + "z": 2.1 + }, + "D9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 47.2, + "z": 2.1 + }, + "E1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 38.2, + "z": 2.1 + }, + "E10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 38.2, + "z": 2.1 + }, + "E11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 38.2, + "z": 2.1 + }, + "E12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 38.2, + "z": 2.1 + }, + "E2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 38.2, + "z": 2.1 + }, + "E3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 38.2, + "z": 2.1 + }, + "E4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 38.2, + "z": 2.1 + }, + "E5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 38.2, + "z": 2.1 + }, + "E6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 38.2, + "z": 2.1 + }, + "E7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 38.2, + "z": 2.1 + }, + "E8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 38.2, + "z": 2.1 + }, + "E9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 38.2, + "z": 2.1 + }, + "F1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 29.2, + "z": 2.1 + }, + "F10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 29.2, + "z": 2.1 + }, + "F11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 29.2, + "z": 2.1 + }, + "F12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 29.2, + "z": 2.1 + }, + "F2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 29.2, + "z": 2.1 + }, + "F3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 29.2, + "z": 2.1 + }, + "F4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 29.2, + "z": 2.1 + }, + "F5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 29.2, + "z": 2.1 + }, + "F6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 29.2, + "z": 2.1 + }, + "F7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 29.2, + "z": 2.1 + }, + "F8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 29.2, + "z": 2.1 + }, + "F9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 29.2, + "z": 2.1 + }, + "G1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 20.2, + "z": 2.1 + }, + "G10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 20.2, + "z": 2.1 + }, + "G11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 20.2, + "z": 2.1 + }, + "G12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 20.2, + "z": 2.1 + }, + "G2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 20.2, + "z": 2.1 + }, + "G3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 20.2, + "z": 2.1 + }, + "G4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 20.2, + "z": 2.1 + }, + "G5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 20.2, + "z": 2.1 + }, + "G6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 20.2, + "z": 2.1 + }, + "G7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 20.2, + "z": 2.1 + }, + "G8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 20.2, + "z": 2.1 + }, + "G9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 20.2, + "z": 2.1 + }, + "H1": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 14.3, + "y": 11.2, + "z": 2.1 + }, + "H10": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 95.3, + "y": 11.2, + "z": 2.1 + }, + "H11": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 104.3, + "y": 11.2, + "z": 2.1 + }, + "H12": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 113.3, + "y": 11.2, + "z": 2.1 + }, + "H2": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 23.3, + "y": 11.2, + "z": 2.1 + }, + "H3": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 32.3, + "y": 11.2, + "z": 2.1 + }, + "H4": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 41.3, + "y": 11.2, + "z": 2.1 + }, + "H5": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 50.3, + "y": 11.2, + "z": 2.1 + }, + "H6": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 59.3, + "y": 11.2, + "z": 2.1 + }, + "H7": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 68.3, + "y": 11.2, + "z": 2.1 + }, + "H8": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 77.3, + "y": 11.2, + "z": 2.1 + }, + "H9": { + "depth": 41.5, + "diameter": 8.5, + "shape": "circular", + "totalLiquidVolume": 2000, + "x": 86.3, + "y": 11.2, + "z": 2.1 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadModule", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "dd0dee2da1b0019f4d98d523b981fabe", + "notes": [], + "params": { + "location": { + "slotName": "B3" + }, + "model": "absorbanceReaderV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 18.5, + "lidHeight": 60.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Absorbance Plate Reader Module GEN1", + "gripperOffsets": {}, + "labwareOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.65 + }, + "model": "absorbanceReaderV1", + "moduleType": "absorbanceReaderType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": {}, + "ot2_standard": {}, + "ot3_standard": {} + } + }, + "model": "absorbanceReaderV1", + "moduleId": "UUID", + "serialNumber": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "absorbanceReader/openLid", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "0fdcfee21f87b074844138ffaeaa61ee", + "notes": [], + "params": { + "moduleId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "error": { + "createdAt": "TIMESTAMP", + "detail": "Cannot move 'thermoscientificnunc_96_wellplate_2000ul' into plate reader because the maximum allowed labware height is 16mm.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "LabwareMovementNotAllowedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + }, + "id": "UUID", + "key": "02138c5885d43cc1dbadfd58415510c4", + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], + "params": { + "labwareId": "UUID", + "newLocation": { + "moduleId": "UUID" + }, + "strategy": "usingGripper" + }, + "startedAt": "TIMESTAMP", + "status": "failed" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ProtocolCommandFailedError [line 23]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): LabwareMovementNotAllowedError: Cannot move 'thermoscientificnunc_96_wellplate_2000ul' into plate reader because the maximum allowed labware height is 16mm.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "LabwareMovementNotAllowedError: Cannot move 'thermoscientificnunc_96_wellplate_2000ul' into plate reader because the maximum allowed labware height is 16mm.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "Cannot move 'thermoscientificnunc_96_wellplate_2000ul' into plate reader because the maximum allowed labware height is 16mm.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "LabwareMovementNotAllowedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ] + } + ], + "files": [ + { + "name": "Flex_X_v2_21_plate_reader_wrong_plate2.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "id": "UUID", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D2" + } + }, + { + "definitionUri": "opentrons/thermoscientificnunc_96_wellplate_2000ul/1", + "id": "UUID", + "loadName": "thermoscientificnunc_96_wellplate_2000ul", + "location": { + "slotName": "C2" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": { + "protocolName": "plate_reader wrong plate" + }, + "modules": [ + { + "id": "UUID", + "location": { + "slotName": "B3" + }, + "model": "absorbanceReaderV1", + "serialNumber": "UUID" + } + ], + "pipettes": [ + { + "id": "UUID", + "mount": "right", + "pipetteName": "p1000_multi_flex" + } + ], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[718c468b7d][Flex_X_v2_21_plate_reader_no_close_lid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[718c468b7d][Flex_X_v2_21_plate_reader_no_close_lid].json new file mode 100644 index 00000000000..3375ed2fadd --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[718c468b7d][Flex_X_v2_21_plate_reader_no_close_lid].json @@ -0,0 +1,2624 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadModule", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "8511b05ba5565bf0e6dcccd800e2ee23", + "notes": [], + "params": { + "location": { + "slotName": "D3" + }, + "model": "absorbanceReaderV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 18.5, + "lidHeight": 60.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Absorbance Plate Reader Module GEN1", + "gripperOffsets": {}, + "labwareOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.65 + }, + "model": "absorbanceReaderV1", + "moduleType": "absorbanceReaderType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": {}, + "ot2_standard": {}, + "ot3_standard": {} + } + }, + "model": "absorbanceReaderV1", + "moduleId": "UUID", + "serialNumber": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "b9563ff54d4bfe61c469a7da06e79f42", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_200ul_flat", + "location": { + "slotName": "C2" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "701011" + ], + "links": [ + "https://www.nest-biotech.com/cell-culture-plates/59415537.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.56, + "yDimension": 85.36, + "zDimension": 14.3 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 11.8, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 200 µL Flat", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "nest_96_wellplate_200ul_flat" + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_flat_bottom_adapter": { + "x": 0, + "y": 0, + "z": 6.7 + }, + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 5.55 + } + }, + "stackingOffsetWithModule": {}, + "version": 2, + "wells": { + "A1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 74.18, + "z": 3.5 + }, + "A10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 74.18, + "z": 3.5 + }, + "A11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 74.18, + "z": 3.5 + }, + "A12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 74.18, + "z": 3.5 + }, + "A2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 74.18, + "z": 3.5 + }, + "A3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 74.18, + "z": 3.5 + }, + "A4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 74.18, + "z": 3.5 + }, + "A5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 74.18, + "z": 3.5 + }, + "A6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 74.18, + "z": 3.5 + }, + "A7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 74.18, + "z": 3.5 + }, + "A8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 74.18, + "z": 3.5 + }, + "A9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 74.18, + "z": 3.5 + }, + "B1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 65.18, + "z": 3.5 + }, + "B10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 65.18, + "z": 3.5 + }, + "B11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 65.18, + "z": 3.5 + }, + "B12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 65.18, + "z": 3.5 + }, + "B2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 65.18, + "z": 3.5 + }, + "B3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 65.18, + "z": 3.5 + }, + "B4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 65.18, + "z": 3.5 + }, + "B5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 65.18, + "z": 3.5 + }, + "B6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 65.18, + "z": 3.5 + }, + "B7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 65.18, + "z": 3.5 + }, + "B8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 65.18, + "z": 3.5 + }, + "B9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 65.18, + "z": 3.5 + }, + "C1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 56.18, + "z": 3.5 + }, + "C10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 56.18, + "z": 3.5 + }, + "C11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 56.18, + "z": 3.5 + }, + "C12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 56.18, + "z": 3.5 + }, + "C2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 56.18, + "z": 3.5 + }, + "C3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 56.18, + "z": 3.5 + }, + "C4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 56.18, + "z": 3.5 + }, + "C5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 56.18, + "z": 3.5 + }, + "C6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 56.18, + "z": 3.5 + }, + "C7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 56.18, + "z": 3.5 + }, + "C8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 56.18, + "z": 3.5 + }, + "C9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 56.18, + "z": 3.5 + }, + "D1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 47.18, + "z": 3.5 + }, + "D10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 47.18, + "z": 3.5 + }, + "D11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 47.18, + "z": 3.5 + }, + "D12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 47.18, + "z": 3.5 + }, + "D2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 47.18, + "z": 3.5 + }, + "D3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 47.18, + "z": 3.5 + }, + "D4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 47.18, + "z": 3.5 + }, + "D5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 47.18, + "z": 3.5 + }, + "D6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 47.18, + "z": 3.5 + }, + "D7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 47.18, + "z": 3.5 + }, + "D8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 47.18, + "z": 3.5 + }, + "D9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 47.18, + "z": 3.5 + }, + "E1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 38.18, + "z": 3.5 + }, + "E10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 38.18, + "z": 3.5 + }, + "E11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 38.18, + "z": 3.5 + }, + "E12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 38.18, + "z": 3.5 + }, + "E2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 38.18, + "z": 3.5 + }, + "E3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 38.18, + "z": 3.5 + }, + "E4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 38.18, + "z": 3.5 + }, + "E5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 38.18, + "z": 3.5 + }, + "E6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 38.18, + "z": 3.5 + }, + "E7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 38.18, + "z": 3.5 + }, + "E8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 38.18, + "z": 3.5 + }, + "E9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 38.18, + "z": 3.5 + }, + "F1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 29.18, + "z": 3.5 + }, + "F10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 29.18, + "z": 3.5 + }, + "F11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 29.18, + "z": 3.5 + }, + "F12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 29.18, + "z": 3.5 + }, + "F2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 29.18, + "z": 3.5 + }, + "F3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 29.18, + "z": 3.5 + }, + "F4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 29.18, + "z": 3.5 + }, + "F5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 29.18, + "z": 3.5 + }, + "F6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 29.18, + "z": 3.5 + }, + "F7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 29.18, + "z": 3.5 + }, + "F8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 29.18, + "z": 3.5 + }, + "F9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 29.18, + "z": 3.5 + }, + "G1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 20.18, + "z": 3.5 + }, + "G10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 20.18, + "z": 3.5 + }, + "G11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 20.18, + "z": 3.5 + }, + "G12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 20.18, + "z": 3.5 + }, + "G2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 20.18, + "z": 3.5 + }, + "G3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 20.18, + "z": 3.5 + }, + "G4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 20.18, + "z": 3.5 + }, + "G5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 20.18, + "z": 3.5 + }, + "G6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 20.18, + "z": 3.5 + }, + "G7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 20.18, + "z": 3.5 + }, + "G8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 20.18, + "z": 3.5 + }, + "G9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 20.18, + "z": 3.5 + }, + "H1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 11.18, + "z": 3.5 + }, + "H10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 11.18, + "z": 3.5 + }, + "H11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 11.18, + "z": 3.5 + }, + "H12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 11.18, + "z": 3.5 + }, + "H2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 11.18, + "z": 3.5 + }, + "H3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 11.18, + "z": 3.5 + }, + "H4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 11.18, + "z": 3.5 + }, + "H5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 11.18, + "z": 3.5 + }, + "H6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 11.18, + "z": 3.5 + }, + "H7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 11.18, + "z": 3.5 + }, + "H8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 11.18, + "z": 3.5 + }, + "H9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 11.18, + "z": 3.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "07af6536632377008ed92723cc8c5072", + "notes": [], + "params": { + "loadName": "opentrons_flex_96_tiprack_50ul", + "location": { + "slotName": "B2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16.0, + "gripHeightFromLabwareBottom": 23.9, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons Flex 96 Tip Rack 50 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_flex_96_tiprack_50ul", + "quirks": [], + "tipLength": 57.9, + "tipOverlap": 10.5 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + }, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 113.38, + "y": 11.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "diameter": 5.58, + "shape": "circular", + "totalLiquidVolume": 50, + "x": 86.38, + "y": 11.38, + "z": 1.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "07ec09c7938c2a88b56846006440a5ee", + "notes": [], + "params": { + "liquidPresenceDetection": false, + "mount": "right", + "pipetteName": "p50_multi_flex", + "tipOverlapNotAfterVersion": "v3" + }, + "result": { + "pipetteId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "f064d7d6dd08b138fa904ed610358512", + "notes": [], + "params": { + "labwareId": "UUID", + "pipetteId": "UUID", + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 178.38, + "y": 288.38, + "z": 99.0 + }, + "tipDiameter": 5.58, + "tipLength": 48.62, + "tipVolume": 50.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "aspirate", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "00e4e8cd652f128d3542e0e8ee3e531c", + "notes": [], + "params": { + "flowRate": 35.0, + "labwareId": "UUID", + "pipetteId": "UUID", + "volume": 30.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -9.8 + }, + "origin": "top", + "volumeOffset": 0.0 + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 178.28, + "y": 181.18, + "z": 4.5 + }, + "volume": 30.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "dispense", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "f7727e49089030d8d36b4d8af8ff97dc", + "notes": [], + "params": { + "flowRate": 57.0, + "labwareId": "UUID", + "pipetteId": "UUID", + "volume": 30.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -9.8 + }, + "origin": "top", + "volumeOffset": 0.0 + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 178.28, + "y": 172.18, + "z": 4.5 + }, + "volume": 30.0 + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "dropTip", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "3f368c52f4738504283097cb6935a781", + "notes": [], + "params": { + "alternateDropLocation": false, + "labwareId": "UUID", + "pipetteId": "UUID", + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "default" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 178.38, + "y": 288.38, + "z": 57.891000000000005 + } + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "CannotPerformModuleAction [line 34]: Error 4000 GENERAL_ERROR (CannotPerformModuleAction): Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "CannotPerformModuleAction", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ], + "files": [ + { + "name": "Flex_X_v2_21_plate_reader_no_close_lid.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/nest_96_wellplate_200ul_flat/2", + "id": "UUID", + "loadName": "nest_96_wellplate_200ul_flat", + "location": { + "slotName": "C2" + } + }, + { + "definitionUri": "opentrons/opentrons_flex_96_tiprack_50ul/1", + "id": "UUID", + "loadName": "opentrons_flex_96_tiprack_50ul", + "location": { + "slotName": "B2" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": { + "author": "Platform Expansion", + "protocolName": "Absorbance Reader no close lid" + }, + "modules": [ + { + "id": "UUID", + "location": { + "slotName": "D3" + }, + "model": "absorbanceReaderV1", + "serialNumber": "UUID" + } + ], + "pipettes": [ + { + "id": "UUID", + "mount": "right", + "pipetteName": "p50_multi_flex" + } + ], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d16d5dbf0][Flex_X_v2_21_tc_lids_wrong_target].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d16d5dbf0][Flex_X_v2_21_tc_lids_wrong_target].json new file mode 100644 index 00000000000..a5d33bd3325 --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d16d5dbf0][Flex_X_v2_21_tc_lids_wrong_target].json @@ -0,0 +1,2041 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadModule", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "8511b05ba5565bf0e6dcccd800e2ee23", + "notes": [], + "params": { + "location": { + "slotName": "D1" + }, + "model": "temperatureModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 11.7, + "y": 8.75, + "z": 80.09 + }, + "compatibleWith": [ + "temperatureModuleV1" + ], + "dimensions": { + "bareOverallHeight": 84.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Temperature Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -1.45, + "y": -0.15, + "z": 80.09 + }, + "model": "temperatureModuleV2", + "moduleType": "temperatureModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + } + } + } + }, + "model": "temperatureModuleV2", + "moduleId": "UUID", + "serialNumber": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "b9563ff54d4bfe61c469a7da06e79f42", + "notes": [], + "params": { + "loadName": "opentrons_96_deep_well_temp_mod_adapter", + "location": { + "moduleId": "UUID" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0.125, + "y": 0.25, + "z": 0 + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.5, + "zDimension": 21.4 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 Deep Well Temperature Module Adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_deep_well_temp_mod_adapter", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": {} + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "07af6536632377008ed92723cc8c5072", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_2ml_deep", + "location": { + "slotName": "A1" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "503001", + "503501" + ], + "links": [ + "https://www.nest-biotech.com/deep-well-plates/59253726.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.6, + "yDimension": 85.3, + "zDimension": 41 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 21.9, + "gripperOffsets": {}, + "groups": [ + { + "brand": { + "brand": "NEST", + "brandId": [] + }, + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Deep Well Plate 2mL", + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Deep Well Plate 2mL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_2ml_deep", + "magneticModuleEngageHeight": 6.8, + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_deep_well_adapter": { + "x": 0, + "y": 0, + "z": 16.3 + }, + "opentrons_96_deep_well_temp_mod_adapter": { + "x": 0, + "y": 0, + "z": 16.1 + } + }, + "stackingOffsetWithModule": { + "magneticBlockV1": { + "x": 0, + "y": 0, + "z": 2.66 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "A9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 74.15, + "yDimension": 8.2, + "z": 3 + }, + "B1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "B9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 65.15, + "yDimension": 8.2, + "z": 3 + }, + "C1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "C9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 56.15, + "yDimension": 8.2, + "z": 3 + }, + "D1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "D9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 47.15, + "yDimension": 8.2, + "z": 3 + }, + "E1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "E9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 38.15, + "yDimension": 8.2, + "z": 3 + }, + "F1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "F9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 29.15, + "yDimension": 8.2, + "z": 3 + }, + "G1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "G9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 20.15, + "yDimension": 8.2, + "z": 3 + }, + "H1": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 14.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H10": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 95.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H11": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 104.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H12": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 113.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H2": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 23.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H3": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 32.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H4": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 41.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H5": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 50.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H6": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 59.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H7": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 68.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H8": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 77.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + }, + "H9": { + "depth": 38, + "shape": "rectangular", + "totalLiquidVolume": 2000, + "x": 86.3, + "xDimension": 8.2, + "y": 11.15, + "yDimension": 8.2, + "z": 3 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "8eec906a29ce38c4567cc9831a24cd13", + "notes": [], + "params": { + "loadName": "opentrons_flex_deck_riser", + "location": { + "slotName": "B2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": -6.125, + "y": -6.125, + "z": 0 + }, + "dimensions": { + "xDimension": 140, + "yDimension": 98, + "zDimension": 55 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons Flex Deck Riser", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_flex_deck_riser", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": {} + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "b96aaea662be1cd49d7d9b93dedfdd76", + "notes": [], + "params": { + "labwareId": "UUID", + "newLocation": { + "labwareId": "UUID" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "error": { + "createdAt": "TIMESTAMP", + "detail": "ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": { + "args": "('Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.',)", + "class": "ValueError", + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line N, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_labware.py\", line N, in execute\n raise ValueError(\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + }, + "id": "UUID", + "key": "48dfe520259476c2688c469837455156", + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], + "params": { + "displayName": "D2", + "loadName": "opentrons_tough_pcr_auto_sealing_lid", + "location": { + "labwareId": "UUID" + }, + "namespace": "opentrons", + "version": 1 + }, + "startedAt": "TIMESTAMP", + "status": "failed" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ProtocolCommandFailedError [line 17]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): PythonException: ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "PythonException: ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": { + "args": "('Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.',)", + "class": "ValueError", + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line N, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_labware.py\", line N, in execute\n raise ValueError(\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ] + } + ], + "files": [ + { + "name": "Flex_X_v2_21_tc_lids_wrong_target.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_96_deep_well_temp_mod_adapter/1", + "id": "UUID", + "loadName": "opentrons_96_deep_well_temp_mod_adapter", + "location": { + "moduleId": "UUID" + } + }, + { + "definitionUri": "opentrons/nest_96_wellplate_2ml_deep/2", + "id": "UUID", + "loadName": "nest_96_wellplate_2ml_deep", + "location": { + "labwareId": "UUID" + } + }, + { + "definitionUri": "opentrons/opentrons_flex_deck_riser/1", + "id": "UUID", + "loadName": "opentrons_flex_deck_riser", + "location": { + "slotName": "B2" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": {}, + "modules": [ + { + "id": "UUID", + "location": { + "slotName": "D1" + }, + "model": "temperatureModuleV2", + "serialNumber": "UUID" + } + ], + "pipettes": [], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[99c15c6c62][Flex_S_v2_21_tc_lids_happy_path].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[99c15c6c62][Flex_S_v2_21_tc_lids_happy_path].json new file mode 100644 index 00000000000..8644d850edb --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[99c15c6c62][Flex_S_v2_21_tc_lids_happy_path].json @@ -0,0 +1,195 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "73d9d4d55ae8466f3a793ceb70545fa5", + "notes": [], + "params": { + "loadName": "opentrons_flex_deck_riser", + "location": { + "slotName": "B2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": -6.125, + "y": -6.125, + "z": 0 + }, + "dimensions": { + "xDimension": 140, + "yDimension": 98, + "zDimension": 55 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons Flex Deck Riser", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_flex_deck_riser", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": {} + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "error": { + "createdAt": "TIMESTAMP", + "detail": "ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": { + "args": "('Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.',)", + "class": "ValueError", + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line N, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_labware.py\", line N, in execute\n raise ValueError(\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + }, + "id": "UUID", + "key": "50de88d471ad3910c29207fb6df4502e", + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], + "params": { + "loadName": "opentrons_tough_pcr_auto_sealing_lid", + "location": { + "labwareId": "UUID" + }, + "namespace": "opentrons", + "version": 1 + }, + "startedAt": "TIMESTAMP", + "status": "failed" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ProtocolCommandFailedError [line 60]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): PythonException: ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "PythonException: ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ValueError: Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.", + "errorCode": "4000", + "errorInfo": { + "args": "('Labware Lid opentrons_tough_pcr_auto_sealing_lid may not be loaded on parent labware Opentrons Flex Deck Riser.',)", + "class": "ValueError", + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line N, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_labware.py\", line N, in execute\n raise ValueError(\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ] + } + ], + "files": [ + { + "name": "Flex_S_v2_21_tc_lids_happy_path.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_flex_deck_riser/1", + "id": "UUID", + "loadName": "opentrons_flex_deck_riser", + "location": { + "slotName": "B2" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": { + "protocolName": "Opentrons Flex Deck Riser with TC Lids Test" + }, + "modules": [], + "pipettes": [], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a5ba8297e3][Flex_X_v2_21_plate_reader_no_trash].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a5ba8297e3][Flex_X_v2_21_plate_reader_no_trash].json new file mode 100644 index 00000000000..f10341a2a2a --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a5ba8297e3][Flex_X_v2_21_plate_reader_no_trash].json @@ -0,0 +1,2541 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "73d9d4d55ae8466f3a793ceb70545fa5", + "notes": [], + "params": { + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16.0, + "gripHeightFromLabwareBottom": 23.9, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "quirks": [], + "tipLength": 95.6, + "tipOverlap": 10.5 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + }, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "bd403a1c851a75b4b68ce34796d713fa", + "notes": [], + "params": { + "liquidPresenceDetection": false, + "mount": "right", + "pipetteName": "p1000_multi_flex", + "tipOverlapNotAfterVersion": "v3" + }, + "result": { + "pipetteId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "configureNozzleLayout", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "2c37ad797da7df791b57a7843a203e88", + "notes": [], + "params": { + "configurationParams": { + "primaryNozzle": "H1", + "style": "SINGLE" + }, + "pipetteId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "691afd54dfa7982fb89e5f77c763bfd4", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_200ul_flat", + "location": { + "slotName": "C1" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "701011" + ], + "links": [ + "https://www.nest-biotech.com/cell-culture-plates/59415537.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.56, + "yDimension": 85.36, + "zDimension": 14.3 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 11.8, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 200 µL Flat", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "nest_96_wellplate_200ul_flat" + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_flat_bottom_adapter": { + "x": 0, + "y": 0, + "z": 6.7 + }, + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 5.55 + } + }, + "stackingOffsetWithModule": {}, + "version": 2, + "wells": { + "A1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 74.18, + "z": 3.5 + }, + "A10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 74.18, + "z": 3.5 + }, + "A11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 74.18, + "z": 3.5 + }, + "A12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 74.18, + "z": 3.5 + }, + "A2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 74.18, + "z": 3.5 + }, + "A3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 74.18, + "z": 3.5 + }, + "A4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 74.18, + "z": 3.5 + }, + "A5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 74.18, + "z": 3.5 + }, + "A6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 74.18, + "z": 3.5 + }, + "A7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 74.18, + "z": 3.5 + }, + "A8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 74.18, + "z": 3.5 + }, + "A9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 74.18, + "z": 3.5 + }, + "B1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 65.18, + "z": 3.5 + }, + "B10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 65.18, + "z": 3.5 + }, + "B11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 65.18, + "z": 3.5 + }, + "B12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 65.18, + "z": 3.5 + }, + "B2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 65.18, + "z": 3.5 + }, + "B3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 65.18, + "z": 3.5 + }, + "B4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 65.18, + "z": 3.5 + }, + "B5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 65.18, + "z": 3.5 + }, + "B6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 65.18, + "z": 3.5 + }, + "B7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 65.18, + "z": 3.5 + }, + "B8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 65.18, + "z": 3.5 + }, + "B9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 65.18, + "z": 3.5 + }, + "C1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 56.18, + "z": 3.5 + }, + "C10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 56.18, + "z": 3.5 + }, + "C11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 56.18, + "z": 3.5 + }, + "C12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 56.18, + "z": 3.5 + }, + "C2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 56.18, + "z": 3.5 + }, + "C3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 56.18, + "z": 3.5 + }, + "C4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 56.18, + "z": 3.5 + }, + "C5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 56.18, + "z": 3.5 + }, + "C6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 56.18, + "z": 3.5 + }, + "C7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 56.18, + "z": 3.5 + }, + "C8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 56.18, + "z": 3.5 + }, + "C9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 56.18, + "z": 3.5 + }, + "D1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 47.18, + "z": 3.5 + }, + "D10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 47.18, + "z": 3.5 + }, + "D11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 47.18, + "z": 3.5 + }, + "D12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 47.18, + "z": 3.5 + }, + "D2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 47.18, + "z": 3.5 + }, + "D3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 47.18, + "z": 3.5 + }, + "D4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 47.18, + "z": 3.5 + }, + "D5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 47.18, + "z": 3.5 + }, + "D6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 47.18, + "z": 3.5 + }, + "D7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 47.18, + "z": 3.5 + }, + "D8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 47.18, + "z": 3.5 + }, + "D9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 47.18, + "z": 3.5 + }, + "E1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 38.18, + "z": 3.5 + }, + "E10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 38.18, + "z": 3.5 + }, + "E11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 38.18, + "z": 3.5 + }, + "E12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 38.18, + "z": 3.5 + }, + "E2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 38.18, + "z": 3.5 + }, + "E3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 38.18, + "z": 3.5 + }, + "E4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 38.18, + "z": 3.5 + }, + "E5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 38.18, + "z": 3.5 + }, + "E6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 38.18, + "z": 3.5 + }, + "E7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 38.18, + "z": 3.5 + }, + "E8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 38.18, + "z": 3.5 + }, + "E9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 38.18, + "z": 3.5 + }, + "F1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 29.18, + "z": 3.5 + }, + "F10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 29.18, + "z": 3.5 + }, + "F11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 29.18, + "z": 3.5 + }, + "F12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 29.18, + "z": 3.5 + }, + "F2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 29.18, + "z": 3.5 + }, + "F3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 29.18, + "z": 3.5 + }, + "F4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 29.18, + "z": 3.5 + }, + "F5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 29.18, + "z": 3.5 + }, + "F6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 29.18, + "z": 3.5 + }, + "F7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 29.18, + "z": 3.5 + }, + "F8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 29.18, + "z": 3.5 + }, + "F9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 29.18, + "z": 3.5 + }, + "G1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 20.18, + "z": 3.5 + }, + "G10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 20.18, + "z": 3.5 + }, + "G11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 20.18, + "z": 3.5 + }, + "G12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 20.18, + "z": 3.5 + }, + "G2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 20.18, + "z": 3.5 + }, + "G3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 20.18, + "z": 3.5 + }, + "G4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 20.18, + "z": 3.5 + }, + "G5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 20.18, + "z": 3.5 + }, + "G6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 20.18, + "z": 3.5 + }, + "G7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 20.18, + "z": 3.5 + }, + "G8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 20.18, + "z": 3.5 + }, + "G9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 20.18, + "z": 3.5 + }, + "H1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 11.18, + "z": 3.5 + }, + "H10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 11.18, + "z": 3.5 + }, + "H11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 11.18, + "z": 3.5 + }, + "H12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 11.18, + "z": 3.5 + }, + "H2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 11.18, + "z": 3.5 + }, + "H3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 11.18, + "z": 3.5 + }, + "H4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 11.18, + "z": 3.5 + }, + "H5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 11.18, + "z": 3.5 + }, + "H6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 11.18, + "z": 3.5 + }, + "H7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 11.18, + "z": 3.5 + }, + "H8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 11.18, + "z": 3.5 + }, + "H9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 11.18, + "z": 3.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadModule", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "dd0dee2da1b0019f4d98d523b981fabe", + "notes": [], + "params": { + "location": { + "slotName": "B3" + }, + "model": "absorbanceReaderV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 18.5, + "lidHeight": 60.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Absorbance Plate Reader Module GEN1", + "gripperOffsets": {}, + "labwareOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.65 + }, + "model": "absorbanceReaderV1", + "moduleType": "absorbanceReaderType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": {}, + "ot2_standard": {}, + "ot3_standard": {} + } + }, + "model": "absorbanceReaderV1", + "moduleId": "UUID", + "serialNumber": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "absorbanceReader/openLid", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "0fdcfee21f87b074844138ffaeaa61ee", + "notes": [], + "params": { + "moduleId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "02138c5885d43cc1dbadfd58415510c4", + "notes": [], + "params": { + "labwareId": "UUID", + "newLocation": { + "moduleId": "UUID" + }, + "strategy": "usingGripper" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "LabwareMovementNotAllowedError [line 24]: Error 4000 GENERAL_ERROR (LabwareMovementNotAllowedError): Can only dispose of tips and Lid-type labware in a Trash Bin. Did you mean to use a Waste Chute?", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "Can only dispose of tips and Lid-type labware in a Trash Bin. Did you mean to use a Waste Chute?", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "LabwareMovementNotAllowedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ], + "files": [ + { + "name": "Flex_X_v2_21_plate_reader_no_trash.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "id": "UUID", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D2" + } + }, + { + "definitionUri": "opentrons/nest_96_wellplate_200ul_flat/2", + "id": "UUID", + "loadName": "nest_96_wellplate_200ul_flat", + "location": { + "moduleId": "UUID" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": { + "protocolName": "plate_reader no trash" + }, + "modules": [ + { + "id": "UUID", + "location": { + "slotName": "B3" + }, + "model": "absorbanceReaderV1", + "serialNumber": "UUID" + } + ], + "pipettes": [ + { + "id": "UUID", + "mount": "right", + "pipetteName": "p1000_multi_flex" + } + ], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ba14bd77a8][Flex_X_v2_21_plate_reader_bad_slot].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ba14bd77a8][Flex_X_v2_21_plate_reader_bad_slot].json new file mode 100644 index 00000000000..d9a486fcf9a --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ba14bd77a8][Flex_X_v2_21_plate_reader_bad_slot].json @@ -0,0 +1,2502 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "73d9d4d55ae8466f3a793ceb70545fa5", + "notes": [], + "params": { + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D3" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16.0, + "gripHeightFromLabwareBottom": 23.9, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "quirks": [], + "tipLength": 95.6, + "tipOverlap": 10.5 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + }, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "bd403a1c851a75b4b68ce34796d713fa", + "notes": [], + "params": { + "liquidPresenceDetection": false, + "mount": "right", + "pipetteName": "p1000_multi_flex", + "tipOverlapNotAfterVersion": "v3" + }, + "result": { + "pipetteId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "configureNozzleLayout", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "2c37ad797da7df791b57a7843a203e88", + "notes": [], + "params": { + "configurationParams": { + "primaryNozzle": "H1", + "style": "SINGLE" + }, + "pipetteId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "691afd54dfa7982fb89e5f77c763bfd4", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_200ul_flat", + "location": { + "slotName": "D1" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "701011" + ], + "links": [ + "https://www.nest-biotech.com/cell-culture-plates/59415537.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.56, + "yDimension": 85.36, + "zDimension": 14.3 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 11.8, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 200 µL Flat", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "nest_96_wellplate_200ul_flat" + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_flat_bottom_adapter": { + "x": 0, + "y": 0, + "z": 6.7 + }, + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 5.55 + } + }, + "stackingOffsetWithModule": {}, + "version": 2, + "wells": { + "A1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 74.18, + "z": 3.5 + }, + "A10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 74.18, + "z": 3.5 + }, + "A11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 74.18, + "z": 3.5 + }, + "A12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 74.18, + "z": 3.5 + }, + "A2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 74.18, + "z": 3.5 + }, + "A3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 74.18, + "z": 3.5 + }, + "A4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 74.18, + "z": 3.5 + }, + "A5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 74.18, + "z": 3.5 + }, + "A6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 74.18, + "z": 3.5 + }, + "A7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 74.18, + "z": 3.5 + }, + "A8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 74.18, + "z": 3.5 + }, + "A9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 74.18, + "z": 3.5 + }, + "B1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 65.18, + "z": 3.5 + }, + "B10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 65.18, + "z": 3.5 + }, + "B11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 65.18, + "z": 3.5 + }, + "B12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 65.18, + "z": 3.5 + }, + "B2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 65.18, + "z": 3.5 + }, + "B3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 65.18, + "z": 3.5 + }, + "B4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 65.18, + "z": 3.5 + }, + "B5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 65.18, + "z": 3.5 + }, + "B6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 65.18, + "z": 3.5 + }, + "B7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 65.18, + "z": 3.5 + }, + "B8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 65.18, + "z": 3.5 + }, + "B9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 65.18, + "z": 3.5 + }, + "C1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 56.18, + "z": 3.5 + }, + "C10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 56.18, + "z": 3.5 + }, + "C11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 56.18, + "z": 3.5 + }, + "C12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 56.18, + "z": 3.5 + }, + "C2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 56.18, + "z": 3.5 + }, + "C3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 56.18, + "z": 3.5 + }, + "C4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 56.18, + "z": 3.5 + }, + "C5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 56.18, + "z": 3.5 + }, + "C6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 56.18, + "z": 3.5 + }, + "C7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 56.18, + "z": 3.5 + }, + "C8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 56.18, + "z": 3.5 + }, + "C9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 56.18, + "z": 3.5 + }, + "D1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 47.18, + "z": 3.5 + }, + "D10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 47.18, + "z": 3.5 + }, + "D11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 47.18, + "z": 3.5 + }, + "D12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 47.18, + "z": 3.5 + }, + "D2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 47.18, + "z": 3.5 + }, + "D3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 47.18, + "z": 3.5 + }, + "D4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 47.18, + "z": 3.5 + }, + "D5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 47.18, + "z": 3.5 + }, + "D6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 47.18, + "z": 3.5 + }, + "D7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 47.18, + "z": 3.5 + }, + "D8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 47.18, + "z": 3.5 + }, + "D9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 47.18, + "z": 3.5 + }, + "E1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 38.18, + "z": 3.5 + }, + "E10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 38.18, + "z": 3.5 + }, + "E11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 38.18, + "z": 3.5 + }, + "E12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 38.18, + "z": 3.5 + }, + "E2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 38.18, + "z": 3.5 + }, + "E3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 38.18, + "z": 3.5 + }, + "E4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 38.18, + "z": 3.5 + }, + "E5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 38.18, + "z": 3.5 + }, + "E6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 38.18, + "z": 3.5 + }, + "E7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 38.18, + "z": 3.5 + }, + "E8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 38.18, + "z": 3.5 + }, + "E9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 38.18, + "z": 3.5 + }, + "F1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 29.18, + "z": 3.5 + }, + "F10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 29.18, + "z": 3.5 + }, + "F11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 29.18, + "z": 3.5 + }, + "F12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 29.18, + "z": 3.5 + }, + "F2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 29.18, + "z": 3.5 + }, + "F3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 29.18, + "z": 3.5 + }, + "F4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 29.18, + "z": 3.5 + }, + "F5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 29.18, + "z": 3.5 + }, + "F6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 29.18, + "z": 3.5 + }, + "F7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 29.18, + "z": 3.5 + }, + "F8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 29.18, + "z": 3.5 + }, + "F9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 29.18, + "z": 3.5 + }, + "G1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 20.18, + "z": 3.5 + }, + "G10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 20.18, + "z": 3.5 + }, + "G11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 20.18, + "z": 3.5 + }, + "G12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 20.18, + "z": 3.5 + }, + "G2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 20.18, + "z": 3.5 + }, + "G3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 20.18, + "z": 3.5 + }, + "G4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 20.18, + "z": 3.5 + }, + "G5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 20.18, + "z": 3.5 + }, + "G6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 20.18, + "z": 3.5 + }, + "G7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 20.18, + "z": 3.5 + }, + "G8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 20.18, + "z": 3.5 + }, + "G9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 20.18, + "z": 3.5 + }, + "H1": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 14.28, + "y": 11.18, + "z": 3.5 + }, + "H10": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 95.28, + "y": 11.18, + "z": 3.5 + }, + "H11": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 104.28, + "y": 11.18, + "z": 3.5 + }, + "H12": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 113.28, + "y": 11.18, + "z": 3.5 + }, + "H2": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 23.28, + "y": 11.18, + "z": 3.5 + }, + "H3": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 32.28, + "y": 11.18, + "z": 3.5 + }, + "H4": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 41.28, + "y": 11.18, + "z": 3.5 + }, + "H5": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 50.28, + "y": 11.18, + "z": 3.5 + }, + "H6": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 59.28, + "y": 11.18, + "z": 3.5 + }, + "H7": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 68.28, + "y": 11.18, + "z": 3.5 + }, + "H8": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 77.28, + "y": 11.18, + "z": 3.5 + }, + "H9": { + "depth": 10.8, + "diameter": 6.85, + "shape": "circular", + "totalLiquidVolume": 200, + "x": 86.28, + "y": 11.18, + "z": 3.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadModule", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "error": { + "createdAt": "TIMESTAMP", + "detail": "ValueError: A absorbanceReaderType cannot be loaded into slot C1", + "errorCode": "4000", + "errorInfo": { + "args": "('A absorbanceReaderType cannot be loaded into slot C1',)", + "class": "ValueError", + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line N, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line N, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line N, in _ensure_module_location\n raise ValueError(\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + }, + "id": "UUID", + "key": "dd0dee2da1b0019f4d98d523b981fabe", + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], + "params": { + "location": { + "slotName": "C1" + }, + "model": "absorbanceReaderV1" + }, + "startedAt": "TIMESTAMP", + "status": "failed" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ProtocolCommandFailedError [line 19]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): PythonException: ValueError: A absorbanceReaderType cannot be loaded into slot C1", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "PythonException: ValueError: A absorbanceReaderType cannot be loaded into slot C1", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ValueError: A absorbanceReaderType cannot be loaded into slot C1", + "errorCode": "4000", + "errorInfo": { + "args": "('A absorbanceReaderType cannot be loaded into slot C1',)", + "class": "ValueError", + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line N, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line N, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line N, in _ensure_module_location\n raise ValueError(\n" + }, + "errorType": "PythonException", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ] + } + ], + "files": [ + { + "name": "Flex_X_v2_21_plate_reader_bad_slot.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "id": "UUID", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D3" + } + }, + { + "definitionUri": "opentrons/nest_96_wellplate_200ul_flat/2", + "id": "UUID", + "loadName": "nest_96_wellplate_200ul_flat", + "location": { + "slotName": "D1" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": { + "protocolName": "plate_reader bad slot" + }, + "modules": [], + "pipettes": [ + { + "id": "UUID", + "mount": "right", + "pipetteName": "p1000_multi_flex" + } + ], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fd26a20b4c][Flex_X_v2_21_plate_reader_wrong_plate].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fd26a20b4c][Flex_X_v2_21_plate_reader_wrong_plate].json new file mode 100644 index 00000000000..39c5598de8b --- /dev/null +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fd26a20b4c][Flex_X_v2_21_plate_reader_wrong_plate].json @@ -0,0 +1,1625 @@ +{ + "commandAnnotations": [], + "commands": [ + { + "commandType": "home", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "50c7ae73a4e3f7129874f39dfb514803", + "notes": [], + "params": {}, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "73d9d4d55ae8466f3a793ceb70545fa5", + "notes": [], + "params": { + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D2" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16.0, + "gripHeightFromLabwareBottom": 23.9, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_flex_96_tiprack_1000ul", + "quirks": [], + "tipLength": 95.6, + "tipOverlap": 10.5 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + }, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "bd403a1c851a75b4b68ce34796d713fa", + "notes": [], + "params": { + "liquidPresenceDetection": false, + "mount": "right", + "pipetteName": "p1000_multi_flex", + "tipOverlapNotAfterVersion": "v3" + }, + "result": { + "pipetteId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "configureNozzleLayout", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "2c37ad797da7df791b57a7843a203e88", + "notes": [], + "params": { + "configurationParams": { + "primaryNozzle": "H1", + "style": "SINGLE" + }, + "pipetteId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "691afd54dfa7982fb89e5f77c763bfd4", + "notes": [], + "params": { + "loadName": "corning_12_wellplate_6.9ml_flat", + "location": { + "slotName": "C2" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Corning", + "brandId": [ + "3336", + "3512", + "3513" + ], + "links": [ + "https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3513?clear=true" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.89, + "yDimension": 85.6, + "zDimension": 20.02 + }, + "gripForce": 16.0, + "gripHeightFromLabwareBottom": 18.3, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "Corning 12 Well Plate 6.9 mL Flat", + "displayVolumeUnits": "mL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1" + ], + [ + "A2", + "B2", + "C2" + ], + [ + "A3", + "B3", + "C3" + ], + [ + "A4", + "B4", + "C4" + ] + ], + "parameters": { + "format": "irregular", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "corning_12_wellplate_6.9ml_flat" + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 4.5 + } + }, + "stackingOffsetWithModule": {}, + "version": 2, + "wells": { + "A1": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 24.94, + "y": 68.81, + "z": 2.49 + }, + "A2": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 50.95, + "y": 68.81, + "z": 2.49 + }, + "A3": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 76.96, + "y": 68.81, + "z": 2.49 + }, + "A4": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 102.97, + "y": 68.81, + "z": 2.49 + }, + "B1": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 24.94, + "y": 42.8, + "z": 2.49 + }, + "B2": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 50.95, + "y": 42.8, + "z": 2.49 + }, + "B3": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 76.96, + "y": 42.8, + "z": 2.49 + }, + "B4": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 102.97, + "y": 42.8, + "z": 2.49 + }, + "C1": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 24.94, + "y": 16.79, + "z": 2.49 + }, + "C2": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 50.95, + "y": 16.79, + "z": 2.49 + }, + "C3": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 76.96, + "y": 16.79, + "z": 2.49 + }, + "C4": { + "depth": 17.53, + "diameter": 22.73, + "shape": "circular", + "totalLiquidVolume": 6900, + "x": 102.97, + "y": 16.79, + "z": 2.49 + } + } + }, + "labwareId": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "loadModule", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "dd0dee2da1b0019f4d98d523b981fabe", + "notes": [], + "params": { + "location": { + "slotName": "B3" + }, + "model": "absorbanceReaderV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 18.5, + "lidHeight": 60.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Absorbance Plate Reader Module GEN1", + "gripperOffsets": {}, + "labwareOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.65 + }, + "model": "absorbanceReaderV1", + "moduleType": "absorbanceReaderType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": {}, + "ot2_standard": {}, + "ot3_standard": {} + } + }, + "model": "absorbanceReaderV1", + "moduleId": "UUID", + "serialNumber": "UUID" + }, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "absorbanceReader/openLid", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "id": "UUID", + "key": "0fdcfee21f87b074844138ffaeaa61ee", + "notes": [], + "params": { + "moduleId": "UUID" + }, + "result": {}, + "startedAt": "TIMESTAMP", + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "completedAt": "TIMESTAMP", + "createdAt": "TIMESTAMP", + "error": { + "createdAt": "TIMESTAMP", + "detail": "Cannot move 'corning_12_wellplate_6.9ml_flat' into plate reader because the labware contains 12 wells where 96 wells is expected.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "LabwareMovementNotAllowedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + }, + "id": "UUID", + "key": "02138c5885d43cc1dbadfd58415510c4", + "notes": [ + { + "longMessage": "Handling this command failure with FAIL_RUN.", + "noteKind": "debugErrorRecovery", + "shortMessage": "Handling this command failure with FAIL_RUN.", + "source": "execution" + } + ], + "params": { + "labwareId": "UUID", + "newLocation": { + "moduleId": "UUID" + }, + "strategy": "usingGripper" + }, + "startedAt": "TIMESTAMP", + "status": "failed" + } + ], + "config": { + "apiVersion": [ + 2, + 21 + ], + "protocolType": "python" + }, + "createdAt": "TIMESTAMP", + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "ProtocolCommandFailedError [line 23]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): LabwareMovementNotAllowedError: Cannot move 'corning_12_wellplate_6.9ml_flat' into plate reader because the labware contains 12 wells where 96 wells is expected.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "LabwareMovementNotAllowedError: Cannot move 'corning_12_wellplate_6.9ml_flat' into plate reader because the labware contains 12 wells where 96 wells is expected.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "Cannot move 'corning_12_wellplate_6.9ml_flat' into plate reader because the labware contains 12 wells where 96 wells is expected.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "LabwareMovementNotAllowedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ] + } + ], + "files": [ + { + "name": "Flex_X_v2_21_plate_reader_wrong_plate.py", + "role": "main" + } + ], + "labware": [ + { + "definitionUri": "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "id": "UUID", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "location": { + "slotName": "D2" + } + }, + { + "definitionUri": "opentrons/corning_12_wellplate_6.9ml_flat/2", + "id": "UUID", + "loadName": "corning_12_wellplate_6.9ml_flat", + "location": { + "slotName": "C2" + } + } + ], + "liquidClasses": [], + "liquids": [], + "metadata": { + "protocolName": "plate_reader wrong plate" + }, + "modules": [ + { + "id": "UUID", + "location": { + "slotName": "B3" + }, + "model": "absorbanceReaderV1", + "serialNumber": "UUID" + } + ], + "pipettes": [ + { + "id": "UUID", + "mount": "right", + "pipetteName": "p1000_multi_flex" + } + ], + "result": "not-ok", + "robotType": "OT-3 Standard", + "runTimeParameters": [] +} diff --git a/api/src/opentrons/drivers/flex_stacker/__init__.py b/api/src/opentrons/drivers/flex_stacker/__init__.py index cd4866c179a..66b4cda546b 100644 --- a/api/src/opentrons/drivers/flex_stacker/__init__.py +++ b/api/src/opentrons/drivers/flex_stacker/__init__.py @@ -1,9 +1,12 @@ -from .abstract import AbstractStackerDriver -from .driver import FlexStackerDriver +from .abstract import AbstractFlexStackerDriver +from .driver import FlexStackerDriver, STACKER_MOTION_CONFIG from .simulator import SimulatingDriver +from . import types as FlexStackerTypes __all__ = [ - "AbstractStackerDriver", + "AbstractFlexStackerDriver", "FlexStackerDriver", "SimulatingDriver", + "FlexStackerTypes", + "STACKER_MOTION_CONFIG", ] diff --git a/api/src/opentrons/drivers/flex_stacker/abstract.py b/api/src/opentrons/drivers/flex_stacker/abstract.py index 5ba3cdcb026..222e6715086 100644 --- a/api/src/opentrons/drivers/flex_stacker/abstract.py +++ b/api/src/opentrons/drivers/flex_stacker/abstract.py @@ -1,6 +1,8 @@ -from typing import Protocol +from typing import List, Protocol from .types import ( + LimitSwitchStatus, + MoveResult, StackerAxis, PlatformStatus, Direction, @@ -10,7 +12,7 @@ ) -class AbstractStackerDriver(Protocol): +class AbstractFlexStackerDriver(Protocol): """Protocol for the Stacker driver.""" async def connect(self) -> None: @@ -25,10 +27,6 @@ async def is_connected(self) -> bool: """Check connection to stacker.""" ... - async def update_firmware(self, firmware_file_path: str) -> None: - """Updates the firmware on the device.""" - ... - async def get_device_info(self) -> StackerInfo: """Get Device Info.""" ... @@ -37,10 +35,26 @@ async def set_serial_number(self, sn: str) -> bool: """Set Serial Number.""" ... + async def enable_motors(self, axis: List[StackerAxis]) -> bool: + """Enables the axis motor if present, disables it otherwise.""" + ... + async def stop_motors(self) -> bool: """Stop all motor movement.""" ... + async def set_run_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis peak run current in amps.""" + ... + + async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis hold current in amps.""" + ... + + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: + """Get the motion parameters used by the given axis motor.""" + ... + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -48,6 +62,10 @@ async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> boo """ ... + async def get_limit_switches_status(self) -> LimitSwitchStatus: + """Get limit switch statuses for all axes.""" + ... + async def get_platform_sensor(self, direction: Direction) -> bool: """Get platform sensor status. @@ -68,13 +86,13 @@ async def get_hopper_door_closed(self) -> bool: async def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None - ) -> bool: - """Move axis.""" + ) -> MoveResult: + """Move axis by the given distance in mm.""" ... async def move_to_limit_switch( self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None - ) -> bool: + ) -> MoveResult: """Move until limit switch is triggered.""" ... @@ -87,3 +105,7 @@ async def set_led( ) -> bool: """Set LED color of status bar.""" ... + + async def enter_programming_mode(self) -> None: + """Reboot into programming mode""" + ... diff --git a/api/src/opentrons/drivers/flex_stacker/driver.py b/api/src/opentrons/drivers/flex_stacker/driver.py index 83671023772..366ea08b5f5 100644 --- a/api/src/opentrons/drivers/flex_stacker/driver.py +++ b/api/src/opentrons/drivers/flex_stacker/driver.py @@ -1,13 +1,14 @@ import asyncio import re -from typing import Optional +from typing import List, Optional from opentrons.drivers.command_builder import CommandBuilder from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection -from .abstract import AbstractStackerDriver +from .abstract import AbstractFlexStackerDriver from .types import ( GCODE, + MoveResult, StackerAxis, PlatformStatus, Direction, @@ -28,7 +29,59 @@ GCODE_ROUNDING_PRECISION = 2 -class FlexStackerDriver(AbstractStackerDriver): +STACKER_MOTION_CONFIG = { + StackerAxis.X: { + "home": MoveParams( + StackerAxis.X, + max_speed=10.0, + acceleration=100.0, + max_speed_discont=40, + current=1.5, + ), + "move": MoveParams( + StackerAxis.X, + max_speed=200.0, + acceleration=1500.0, + max_speed_discont=40, + current=1.0, + ), + }, + StackerAxis.Z: { + "home": MoveParams( + StackerAxis.Z, + max_speed=10.0, + acceleration=100.0, + max_speed_discont=40, + current=1.5, + ), + "move": MoveParams( + StackerAxis.Z, + max_speed=200.0, + acceleration=500.0, + max_speed_discont=40, + current=1.5, + ), + }, + StackerAxis.L: { + "home": MoveParams( + StackerAxis.L, + max_speed=100.0, + acceleration=800.0, + max_speed_discont=40, + current=0.8, + ), + "move": MoveParams( + StackerAxis.L, + max_speed=100.0, + acceleration=800.0, + max_speed_discont=40, + current=0.6, + ), + }, +} + + +class FlexStackerDriver(AbstractFlexStackerDriver): """FLEX Stacker driver.""" @classmethod @@ -76,6 +129,27 @@ def parse_door_closed(cls, response: str) -> bool: raise ValueError(f"Incorrect Response for door closed: {response}") return bool(int(match.group(1))) + @classmethod + def parse_move_params(cls, response: str) -> MoveParams: + """Parse move params.""" + field_names = MoveParams.get_fields() + pattern = r"\s".join( + [ + rf"{f}:(?P<{f}>(\d*\.)?\d+)" if f != "M" else rf"{f}:(?P<{f}>[X,Z,L])" + for f in field_names + ] + ) + _RE = re.compile(f"^{GCODE.GET_MOVE_PARAMS} {pattern}$") + m = _RE.match(response) + if not m: + raise ValueError(f"Incorrect Response for move params: {response}") + return MoveParams( + axis=StackerAxis(m.group("M")), + max_speed=float(m.group("V")), + acceleration=float(m.group("A")), + max_speed_discont=float(m.group("D")), + ) + @classmethod def append_move_params( cls, command: CommandBuilder, params: MoveParams | None @@ -148,6 +222,16 @@ async def set_serial_number(self, sn: str) -> bool: raise ValueError(f"Incorrect Response for set serial number: {resp}") return True + async def enable_motors(self, axis: List[StackerAxis]) -> bool: + """Enables the axis motor if present, disables it otherwise.""" + command = GCODE.ENABLE_MOTORS.build_command() + for a in axis: + command.add_element(a.name) + resp = await self._connection.send_command(command) + if not re.match(rf"^{GCODE.ENABLE_MOTORS}$", resp): + raise ValueError(f"Incorrect Response for enable motors: {resp}") + return True + async def stop_motors(self) -> bool: """Stop all motor movement.""" resp = await self._connection.send_command(GCODE.STOP_MOTORS.build_command()) @@ -155,6 +239,31 @@ async def stop_motors(self) -> bool: raise ValueError(f"Incorrect Response for stop motors: {resp}") return True + async def set_run_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis peak run current in amps.""" + resp = await self._connection.send_command( + GCODE.SET_RUN_CURRENT.build_command().add_float(axis.name, current) + ) + if not re.match(rf"^{GCODE.SET_RUN_CURRENT}$", resp): + raise ValueError(f"Incorrect Response for set run current: {resp}") + return True + + async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis hold current in amps.""" + resp = await self._connection.send_command( + GCODE.SET_IHOLD_CURRENT.build_command().add_float(axis.name, current) + ) + if not re.match(rf"^{GCODE.SET_IHOLD_CURRENT}$", resp): + raise ValueError(f"Incorrect Response for set ihold current: {resp}") + return True + + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: + """Get the motion parameters used by the given axis motor.""" + response = await self._connection.send_command( + GCODE.GET_MOVE_PARAMS.build_command().add_element(axis.name) + ) + return self.parse_move_params(response) + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -197,8 +306,8 @@ async def get_hopper_door_closed(self) -> bool: async def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None - ) -> bool: - """Move axis.""" + ) -> MoveResult: + """Move axis by the given distance in mm.""" command = self.append_move_params( GCODE.MOVE_TO.build_command().add_float( axis.name, distance, GCODE_ROUNDING_PRECISION @@ -208,11 +317,12 @@ async def move_in_mm( resp = await self._connection.send_command(command) if not re.match(rf"^{GCODE.MOVE_TO}$", resp): raise ValueError(f"Incorrect Response for move to: {resp}") - return True + # TODO: handle STALL_ERROR + return MoveResult.NO_ERROR async def move_to_limit_switch( self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None - ) -> bool: + ) -> MoveResult: """Move until limit switch is triggered.""" command = self.append_move_params( GCODE.MOVE_TO_SWITCH.build_command().add_int(axis.name, direction.value), @@ -221,7 +331,8 @@ async def move_to_limit_switch( resp = await self._connection.send_command(command) if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp): raise ValueError(f"Incorrect Response for move to switch: {resp}") - return True + # TODO: handle STALL_ERROR + return MoveResult.NO_ERROR async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: """Home axis.""" @@ -254,7 +365,8 @@ async def set_led( raise ValueError(f"Incorrect Response for set led: {resp}") return True - async def update_firmware(self, firmware_file_path: str) -> None: - """Updates the firmware on the device.""" - # TODO: Implement firmware update - pass + async def enter_programming_mode(self) -> None: + """Reboot into programming mode""" + command = GCODE.ENTER_BOOTLOADER.build_command() + await self._connection.send_dfu_command(command) + await self._connection.close() diff --git a/api/src/opentrons/drivers/flex_stacker/simulator.py b/api/src/opentrons/drivers/flex_stacker/simulator.py index 1e0b59b19de..1ceedabf146 100644 --- a/api/src/opentrons/drivers/flex_stacker/simulator.py +++ b/api/src/opentrons/drivers/flex_stacker/simulator.py @@ -1,9 +1,11 @@ -from typing import Optional +from typing import List, Optional from opentrons.util.async_helpers import ensure_yield -from .abstract import AbstractStackerDriver +from .abstract import AbstractFlexStackerDriver from .types import ( + LEDColor, + MoveResult, StackerAxis, PlatformStatus, Direction, @@ -14,7 +16,7 @@ ) -class SimulatingDriver(AbstractStackerDriver): +class SimulatingDriver(AbstractFlexStackerDriver): """FLEX Stacker driver simulator.""" def __init__(self, serial_number: Optional[str] = None) -> None: @@ -60,11 +62,27 @@ async def set_serial_number(self, sn: str) -> bool: """Set Serial Number.""" return True + async def enable_motors(self, axis: List[StackerAxis]) -> bool: + """Enables the axis motor if present, disables it otherwise.""" + return True + @ensure_yield - async def stop_motor(self) -> bool: - """Stop motor movement.""" + async def stop_motors(self) -> bool: + """Stop all motor movement.""" + return True + + async def set_run_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis peak run current in amps.""" + return True + + async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis hold current in amps.""" return True + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: + """Get the motion parameters used by the given axis motor.""" + return MoveParams(axis, 1, 1, 1) + @ensure_yield async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -78,12 +96,15 @@ async def get_limit_switches_status(self) -> LimitSwitchStatus: """Get limit switch statuses for all axes.""" return self._limit_switch_status - @ensure_yield - async def get_platform_sensor_status(self) -> PlatformStatus: + async def get_platform_sensor(self, direction: Direction) -> bool: """Get platform sensor status. - :return: True if platform is detected, False otherwise + :return: True if platform is present, False otherwise """ + return True + + async def get_platform_status(self) -> PlatformStatus: + """Get platform status.""" return self._platform_sensor_status @ensure_yield @@ -97,13 +118,27 @@ async def get_hopper_door_closed(self) -> bool: @ensure_yield async def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None - ) -> bool: - """Move axis.""" - return True + ) -> MoveResult: + """Move axis by the given distance in mm.""" + return MoveResult.NO_ERROR @ensure_yield async def move_to_limit_switch( self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None - ) -> bool: + ) -> MoveResult: """Move until limit switch is triggered.""" + return MoveResult.NO_ERROR + + async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + """Home axis.""" return True + + async def set_led( + self, power: float, color: LEDColor | None = None, external: bool | None = None + ) -> bool: + """Set LED color.""" + return True + + async def enter_programming_mode(self) -> None: + """Reboot into programming mode""" + pass diff --git a/api/src/opentrons/drivers/flex_stacker/types.py b/api/src/opentrons/drivers/flex_stacker/types.py index 4035aaaa755..9f8e8825b93 100644 --- a/api/src/opentrons/drivers/flex_stacker/types.py +++ b/api/src/opentrons/drivers/flex_stacker/types.py @@ -1,6 +1,6 @@ from enum import Enum from dataclasses import dataclass, fields -from typing import List +from typing import List, Dict, Optional from opentrons.drivers.command_builder import CommandBuilder @@ -11,13 +11,17 @@ class GCODE(str, Enum): MOVE_TO_SWITCH = "G5" HOME_AXIS = "G28" STOP_MOTORS = "M0" + ENABLE_MOTORS = "M17" GET_RESET_REASON = "M114" DEVICE_INFO = "M115" GET_LIMIT_SWITCH = "M119" - SET_LED = "M200" + GET_MOVE_PARAMS = "M120" GET_PLATFORM_SENSOR = "M121" GET_DOOR_SWITCH = "M122" + SET_LED = "M200" SET_SERIAL_NUMBER = "M996" + SET_RUN_CURRENT = "M906" + SET_IHOLD_CURRENT = "M907" ENTER_BOOTLOADER = "dfu" def build_command(self) -> CommandBuilder: @@ -45,6 +49,14 @@ class StackerInfo: hw: HardwareRevision sn: str + def to_dict(self) -> Dict[str, str]: + """Build command.""" + return { + "serial": self.sn, + "version": self.fw, + "model": self.hw.value, + } + class StackerAxis(Enum): """Stacker Axis.""" @@ -128,11 +140,33 @@ def get(self, direction: Direction) -> bool: """Get platform status.""" return self.E if direction == Direction.EXTENT else self.R + def to_dict(self) -> Dict[str, bool]: + """Dict of the data.""" + return { + "extent": self.E, + "retract": self.R, + } + @dataclass class MoveParams: """Move Parameters.""" - max_speed: float | None = None - acceleration: float | None = None - max_speed_discont: float | None = None + axis: Optional[StackerAxis] = None + max_speed: Optional[float] = None + acceleration: Optional[float] = None + max_speed_discont: Optional[float] = None + current: Optional[float] = 0 + + @classmethod + def get_fields(cls) -> List[str]: + """Get parsing fields.""" + return ["M", "V", "A", "D"] + + +class MoveResult(str, Enum): + """The result of a move command.""" + + NO_ERROR = "ok" + STALL_ERROR = "stall" + UNKNOWN_ERROR = "unknown" diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 84ffbffd8da..70d895560ee 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1397,7 +1397,11 @@ async def clean_up(self) -> None: return if hasattr(self, "_event_watcher"): - if loop.is_running() and self._event_watcher: + if ( + loop.is_running() + and self._event_watcher + and not self._event_watcher.closed + ): self._event_watcher.close() messenger = getattr(self, "_messenger", None) diff --git a/api/src/opentrons/hardware_control/modules/__init__.py b/api/src/opentrons/hardware_control/modules/__init__.py index 67a6c442f39..985b5970fec 100644 --- a/api/src/opentrons/hardware_control/modules/__init__.py +++ b/api/src/opentrons/hardware_control/modules/__init__.py @@ -4,6 +4,7 @@ from .thermocycler import Thermocycler from .heater_shaker import HeaterShaker from .absorbance_reader import AbsorbanceReader +from .flex_stacker import FlexStacker from .update import update_firmware from .utils import MODULE_TYPE_BY_NAME, build from .types import ( @@ -19,8 +20,13 @@ MagneticStatus, HeaterShakerStatus, AbsorbanceReaderStatus, + PlatformState, + StackerAxisState, + FlexStackerStatus, SpeedStatus, LiveData, + ModuleData, + ModuleDataValidator, ) from .errors import ( UpdateError, @@ -51,8 +57,13 @@ "HeaterShakerStatus", "SpeedStatus", "LiveData", + "ModuleData", + "ModuleDataValidator", "AbsorbanceReader", "AbsorbanceReaderStatus", "AbsorbanceReaderDisconnectedError", - "ModuleDisconnectedCallback", + "FlexStacker", + "FlexStackerStatus", + "PlatformState", + "StackerAxisState", ] diff --git a/api/src/opentrons/hardware_control/modules/absorbance_reader.py b/api/src/opentrons/hardware_control/modules/absorbance_reader.py index ec4a80b7f60..a12548ef6ed 100644 --- a/api/src/opentrons/hardware_control/modules/absorbance_reader.py +++ b/api/src/opentrons/hardware_control/modules/absorbance_reader.py @@ -24,6 +24,7 @@ ModuleType, AbsorbanceReaderStatus, LiveData, + AbsorbanceReaderData, UploadFunction, ) from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError @@ -243,17 +244,18 @@ def device_info(self) -> Mapping[str, str]: def live_data(self) -> LiveData: """Return a dict of the module's dynamic information""" conf = self._measurement_config.data if self._measurement_config else dict() + data: AbsorbanceReaderData = { + "uptime": self.uptime, + "deviceStatus": self.status.value, + "lidStatus": self.lid_status.value, + "platePresence": self.plate_presence.value, + "measureMode": conf.get("measureMode", ""), + "sampleWavelengths": conf.get("sampleWavelengths", []), + "referenceWavelength": conf.get("referenceWavelength", 0), + } return { "status": self.status.value, - "data": { - "uptime": self.uptime, - "deviceStatus": self.status.value, - "lidStatus": self.lid_status.value, - "platePresence": self.plate_presence.value, - "measureMode": conf.get("measureMode", ""), - "sampleWavelengths": conf.get("sampleWavelengths", []), - "referenceWavelength": conf.get("referenceWavelength", 0), - }, + "data": data, } @property diff --git a/api/src/opentrons/hardware_control/modules/flex_stacker.py b/api/src/opentrons/hardware_control/modules/flex_stacker.py new file mode 100644 index 00000000000..295311e05ca --- /dev/null +++ b/api/src/opentrons/hardware_control/modules/flex_stacker.py @@ -0,0 +1,435 @@ +from __future__ import annotations + +import asyncio +import logging +from typing import Dict, Optional, Mapping + +from opentrons.drivers.flex_stacker.types import ( + Direction, + MoveParams, + MoveResult, + StackerAxis, +) +from opentrons.drivers.rpi_drivers.types import USBPort +from opentrons.drivers.flex_stacker.driver import ( + STACKER_MOTION_CONFIG, + FlexStackerDriver, +) +from opentrons.drivers.flex_stacker.abstract import AbstractFlexStackerDriver +from opentrons.drivers.flex_stacker.simulator import SimulatingDriver +from opentrons.hardware_control.execution_manager import ExecutionManager +from opentrons.hardware_control.poller import Reader, Poller +from opentrons.hardware_control.modules import mod_abc, update +from opentrons.hardware_control.modules.types import ( + FlexStackerStatus, + HopperDoorState, + LatchState, + ModuleDisconnectedCallback, + ModuleType, + PlatformState, + StackerAxisState, + UploadFunction, + LiveData, + FlexStackerData, +) + +log = logging.getLogger(__name__) + +POLL_PERIOD = 1.0 +SIMULATING_POLL_PERIOD = POLL_PERIOD / 20.0 + +DFU_PID = "df11" + +# Maximum distance in mm the axis can travel. +MAX_TRAVEL = { + StackerAxis.X: 192.5, + StackerAxis.Z: 136.0, + StackerAxis.L: 23.0, +} + +# The offset in mm to subtract from MAX_TRAVEL when moving an axis before we home. +# This lets us use `move_axis` to move fast, leaving the axis OFFSET mm +# from the limit switch. Then we can use `home_axis` to move the axis the rest +# of the way until we trigger the expected limit switch. +OFFSET_SM = 5.0 +OFFSET_MD = 10.0 +OFFSET_LG = 20.0 + + +class FlexStacker(mod_abc.AbstractModule): + """Hardware control interface for an attached Flex-Stacker module.""" + + MODULE_TYPE = ModuleType.FLEX_STACKER + + @classmethod + async def build( + cls, + port: str, + usb_port: USBPort, + execution_manager: ExecutionManager, + hw_control_loop: asyncio.AbstractEventLoop, + poll_interval_seconds: Optional[float] = None, + simulating: bool = False, + sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, + disconnected_callback: ModuleDisconnectedCallback = None, + ) -> "FlexStacker": + """ + Build a FlexStacker + + Args: + port: The port to connect to + usb_port: USB Port + execution_manager: Execution manager. + hw_control_loop: The event loop running in the hardware control thread. + poll_interval_seconds: Poll interval override. + simulating: whether to build a simulating driver + loop: Loop + sim_model: The model name used by simulator + disconnected_callback: Callback to inform the module controller that the device was disconnected + + Returns: + FlexStacker instance + """ + driver: AbstractFlexStackerDriver + if not simulating: + driver = await FlexStackerDriver.create(port=port, loop=hw_control_loop) + poll_interval_seconds = poll_interval_seconds or POLL_PERIOD + else: + driver = SimulatingDriver(serial_number=sim_serial_number) + poll_interval_seconds = poll_interval_seconds or SIMULATING_POLL_PERIOD + + reader = FlexStackerReader(driver=driver) + poller = Poller(reader=reader, interval=poll_interval_seconds) + module = cls( + port=port, + usb_port=usb_port, + driver=driver, + reader=reader, + poller=poller, + device_info=(await driver.get_device_info()).to_dict(), + hw_control_loop=hw_control_loop, + execution_manager=execution_manager, + disconnected_callback=disconnected_callback, + ) + + try: + await poller.start() + except Exception: + log.exception(f"First read of Flex-Stacker on port {port} failed") + + return module + + def __init__( + self, + port: str, + usb_port: USBPort, + execution_manager: ExecutionManager, + driver: AbstractFlexStackerDriver, + reader: FlexStackerReader, + poller: Poller, + device_info: Mapping[str, str], + hw_control_loop: asyncio.AbstractEventLoop, + disconnected_callback: ModuleDisconnectedCallback = None, + ): + super().__init__( + port=port, + usb_port=usb_port, + hw_control_loop=hw_control_loop, + execution_manager=execution_manager, + disconnected_callback=disconnected_callback, + ) + self._device_info = device_info + self._driver = driver + self._reader = reader + self._poller = poller + self._stacker_status = FlexStackerStatus.IDLE + + async def cleanup(self) -> None: + """Stop the poller task""" + await self._poller.stop() + await self._driver.disconnect() + + @classmethod + def name(cls) -> str: + """Used for picking up serial port symlinks""" + return "flexstacker" + + def firmware_prefix(self) -> str: + """The prefix used for looking up firmware""" + return "flex-stacker" + + @staticmethod + def _model_from_revision(revision: Optional[str]) -> str: + """Defines the revision -> model mapping""" + return "flexStackerModuleV1" + + def model(self) -> str: + return self._model_from_revision(self._device_info.get("model")) + + @property + def latch_state(self) -> LatchState: + """The state of the latch.""" + return LatchState.from_state(self.limit_switch_status[StackerAxis.L]) + + @property + def platform_state(self) -> PlatformState: + """The state of the platform.""" + return self._reader.platform_state + + @property + def hopper_door_state(self) -> HopperDoorState: + """The status of the hopper door.""" + return HopperDoorState.from_state(self._reader.hopper_door_closed) + + @property + def limit_switch_status(self) -> Dict[StackerAxis, StackerAxisState]: + """The status of the Limit switches.""" + return self._reader.limit_switch_status + + @property + def device_info(self) -> Mapping[str, str]: + return self._device_info + + @property + def status(self) -> FlexStackerStatus: + """Module status or error state details.""" + return self._stacker_status + + @property + def is_simulated(self) -> bool: + return isinstance(self._driver, SimulatingDriver) + + @property + def live_data(self) -> LiveData: + data: FlexStackerData = { + "latchState": self.latch_state.value, + "platformState": self.platform_state.value, + "hopperDoorState": self.hopper_door_state.value, + "axisStateX": self.limit_switch_status[StackerAxis.X].value, + "axisStateZ": self.limit_switch_status[StackerAxis.Z].value, + "errorDetails": self._reader.error, + } + return {"status": self.status.value, "data": data} + + async def prep_for_update(self) -> str: + await self._poller.stop() + await self._driver.stop_motors() + await self._driver.enter_programming_mode() + # flex stacker has three unique "devices" over DFU + dfu_info = await update.find_dfu_device(pid=DFU_PID, expected_device_count=3) + return dfu_info + + def bootloader(self) -> UploadFunction: + return update.upload_via_dfu + + async def deactivate(self, must_be_running: bool = True) -> None: + await self._driver.stop_motors() + + async def move_axis( + self, + axis: StackerAxis, + direction: Direction, + distance: float, + speed: Optional[float] = None, + acceleration: Optional[float] = None, + current: Optional[float] = None, + ) -> bool: + """Move the axis in a direction by the given distance in mm.""" + motion_params = STACKER_MOTION_CONFIG[axis]["move"] + await self._driver.set_run_current(axis, current or motion_params.current or 0) + if any([speed, acceleration]): + motion_params.max_speed = speed or motion_params.max_speed + motion_params.acceleration = acceleration or motion_params.acceleration + distance = direction.distance(distance) + success = await self._driver.move_in_mm(axis, distance, params=motion_params) + # TODO: This can return a stall, handle that here + return success == MoveResult.NO_ERROR + + async def home_axis( + self, + axis: StackerAxis, + direction: Direction, + speed: Optional[float] = None, + acceleration: Optional[float] = None, + current: Optional[float] = None, + ) -> bool: + motion_params = STACKER_MOTION_CONFIG[axis]["home"] + await self._driver.set_run_current(axis, current or motion_params.current or 0) + # Set the max hold current for the Z axis + if axis == StackerAxis.Z: + await self._driver.set_ihold_current(axis, 1.8) + if any([speed, acceleration]): + motion_params.max_speed = speed or motion_params.max_speed + motion_params.acceleration = acceleration or motion_params.acceleration + success = await self._driver.move_to_limit_switch( + axis=axis, direction=direction, params=motion_params + ) + # TODO: This can return a stall, handle that here + return success == MoveResult.NO_ERROR + + async def close_latch( + self, + velocity: Optional[float] = None, + acceleration: Optional[float] = None, + ) -> bool: + """Close the latch, dropping any labware its holding.""" + # Dont move the latch if its already closed. + if self.limit_switch_status[StackerAxis.L] == StackerAxisState.EXTENDED: + return True + motion_params = STACKER_MOTION_CONFIG[StackerAxis.L]["move"] + speed = velocity or motion_params.max_speed + accel = acceleration or motion_params.acceleration + success = await self.home_axis( + StackerAxis.L, + Direction.RETRACT, + speed=speed, + acceleration=accel, + ) + # Check that the latch is closed. + await self._reader.get_limit_switch_status() + return ( + success + and self.limit_switch_status[StackerAxis.L] == StackerAxisState.EXTENDED + ) + + async def open_latch( + self, + velocity: Optional[float] = None, + acceleration: Optional[float] = None, + ) -> bool: + """Open the latch.""" + # Dont move the latch if its already opened. + if self.limit_switch_status[StackerAxis.L] == StackerAxisState.RETRACTED: + return True + motion_params = STACKER_MOTION_CONFIG[StackerAxis.L]["move"] + speed = velocity or motion_params.max_speed + accel = acceleration or motion_params.acceleration + distance = MAX_TRAVEL[StackerAxis.L] + # The latch only has one limit switch, so we have to travel a fixed distance + # to open the latch. + success = await self.move_axis( + StackerAxis.L, + Direction.EXTENT, + distance=distance, + speed=speed, + acceleration=accel, + ) + # Check that the latch is opened. + await self._reader.get_limit_switch_status() + axis_state = self.limit_switch_status[StackerAxis.L] + return success and axis_state == StackerAxisState.RETRACTED + + async def dispense_labware(self, labware_height: float) -> bool: + """Dispenses the next labware in the stacker.""" + await self._prepare_for_action() + + # Move platform along the X then Z axis + await self._move_and_home_axis(StackerAxis.X, Direction.RETRACT, OFFSET_SM) + await self._move_and_home_axis(StackerAxis.Z, Direction.EXTENT, OFFSET_SM) + + # Transfer + await self.open_latch() + await self.move_axis(StackerAxis.Z, Direction.RETRACT, (labware_height / 2) + 2) + await self.close_latch() + + # Move platform along the Z then X axis + offset = labware_height / 2 + OFFSET_MD + await self._move_and_home_axis(StackerAxis.Z, Direction.RETRACT, offset) + await self._move_and_home_axis(StackerAxis.X, Direction.EXTENT, OFFSET_SM) + return True + + async def store_labware(self, labware_height: float) -> bool: + """Stores a labware in the stacker.""" + await self._prepare_for_action() + + # Move X then Z axis + distance = MAX_TRAVEL[StackerAxis.Z] - (labware_height / 2) - OFFSET_MD + await self._move_and_home_axis(StackerAxis.X, Direction.RETRACT, OFFSET_SM) + await self.move_axis(StackerAxis.Z, Direction.EXTENT, distance) + + # Transfer + await self.open_latch() + await self.move_axis(StackerAxis.Z, Direction.EXTENT, (labware_height / 2)) + await self.home_axis(StackerAxis.Z, Direction.EXTENT) + await self.close_latch() + + # Move Z then X axis + await self._move_and_home_axis(StackerAxis.Z, Direction.RETRACT, OFFSET_LG) + await self._move_and_home_axis(StackerAxis.X, Direction.EXTENT, OFFSET_SM) + return True + + async def _move_and_home_axis( + self, axis: StackerAxis, direction: Direction, offset: float = 0 + ) -> bool: + distance = MAX_TRAVEL[axis] - offset + await self.move_axis(axis, direction, distance) + return await self.home_axis(axis, direction) + + async def _prepare_for_action(self) -> bool: + """Helper to prepare axis for dispensing or storing labware.""" + # TODO: check if we need to home first + await self.home_axis(StackerAxis.X, Direction.EXTENT) + await self.home_axis(StackerAxis.Z, Direction.RETRACT) + await self.close_latch() + return True + + +class FlexStackerReader(Reader): + error: Optional[str] + + def __init__(self, driver: AbstractFlexStackerDriver) -> None: + self.error: Optional[str] = None + self._driver = driver + self.limit_switch_status = { + axis: StackerAxisState.UNKNOWN for axis in StackerAxis + } + self.platform_state = PlatformState.UNKNOWN + self.hopper_door_closed = False + self.motion_params = {axis: MoveParams(axis=axis) for axis in StackerAxis} + self.get_config = True + + async def read(self) -> None: + await self.get_limit_switch_status() + await self.get_platform_sensor_state() + await self.get_door_closed() + if self.get_config: + await self.get_motion_parameters() + self.get_config = False + self._set_error(None) + + async def get_limit_switch_status(self) -> None: + """Get the limit switch status.""" + status = await self._driver.get_limit_switches_status() + self.limit_switch_status = { + StackerAxis.X: StackerAxisState.from_status(status, StackerAxis.X), + StackerAxis.Z: StackerAxisState.from_status(status, StackerAxis.Z), + StackerAxis.L: StackerAxisState.from_status(status, StackerAxis.L), + } + + async def get_motion_parameters(self) -> None: + """Get the motion parameters used by the axis motors.""" + self.move_params = { + axis: self._driver.get_motion_params(axis) for axis in StackerAxis + } + + async def get_platform_sensor_state(self) -> None: + """Get the platform state.""" + status = await self._driver.get_platform_status() + self.platform_state = PlatformState.from_status(status) + + async def get_door_closed(self) -> None: + """Check if the hopper door is closed.""" + self.hopper_door_closed = await self._driver.get_hopper_door_closed() + + def on_error(self, exception: Exception) -> None: + self._set_error(exception) + + def _set_error(self, exception: Optional[Exception]) -> None: + if exception is None: + self.error = None + else: + try: + self.error = str(exception.args[0]) + except Exception: + self.error = repr(exception) diff --git a/api/src/opentrons/hardware_control/modules/heater_shaker.py b/api/src/opentrons/hardware_control/modules/heater_shaker.py index cc592d3c514..2de119ab4ec 100644 --- a/api/src/opentrons/hardware_control/modules/heater_shaker.py +++ b/api/src/opentrons/hardware_control/modules/heater_shaker.py @@ -21,6 +21,7 @@ HeaterShakerStatus, UploadFunction, LiveData, + HeaterShakerData, ) log = logging.getLogger(__name__) @@ -200,18 +201,19 @@ def device_info(self) -> Mapping[str, str]: @property def live_data(self) -> LiveData: + data: HeaterShakerData = { + "temperatureStatus": self.temperature_status, + "speedStatus": self.speed_status, + "labwareLatchStatus": self.labware_latch_status, + "currentTemp": self.temperature, + "targetTemp": self.target_temperature, + "currentSpeed": self.speed, + "targetSpeed": self.target_speed, + "errorDetails": self._reader.error, + } return { "status": self.status.value, - "data": { - "temperatureStatus": self.temperature_status.value, - "speedStatus": self.speed_status.value, - "labwareLatchStatus": self.labware_latch_status.value, - "currentTemp": self.temperature, - "targetTemp": self.target_temperature, - "currentSpeed": self.speed, - "targetSpeed": self.target_speed, - "errorDetails": self._reader.error, - }, + "data": data, } @property diff --git a/api/src/opentrons/hardware_control/modules/magdeck.py b/api/src/opentrons/hardware_control/modules/magdeck.py index a97afde77b6..a551421e288 100644 --- a/api/src/opentrons/hardware_control/modules/magdeck.py +++ b/api/src/opentrons/hardware_control/modules/magdeck.py @@ -196,9 +196,13 @@ def engaged(self) -> bool: @property def live_data(self) -> types.LiveData: + data: types.MagneticModuleData = { + "engaged": self.engaged, + "height": self.current_height, + } return { "status": self.status, - "data": {"engaged": self.engaged, "height": self.current_height}, + "data": data, } @property diff --git a/api/src/opentrons/hardware_control/modules/tempdeck.py b/api/src/opentrons/hardware_control/modules/tempdeck.py index 1e3b4bba2d5..8e3271d604b 100644 --- a/api/src/opentrons/hardware_control/modules/tempdeck.py +++ b/api/src/opentrons/hardware_control/modules/tempdeck.py @@ -195,9 +195,13 @@ def device_info(self) -> Dict[str, str]: @property def live_data(self) -> types.LiveData: + data: types.TemperatureModuleData = { + "currentTemp": self.temperature, + "targetTemp": self.target, + } return { "status": self.status, - "data": {"currentTemp": self.temperature, "targetTemp": self.target}, + "data": data, } @property diff --git a/api/src/opentrons/hardware_control/modules/thermocycler.py b/api/src/opentrons/hardware_control/modules/thermocycler.py index 4a1b2fe038b..73ab01a4fa8 100644 --- a/api/src/opentrons/hardware_control/modules/thermocycler.py +++ b/api/src/opentrons/hardware_control/modules/thermocycler.py @@ -544,22 +544,23 @@ def current_step_index(self) -> Optional[int]: @property def live_data(self) -> types.LiveData: + data: types.ThermocyclerData = { + "lid": self.lid_status, + "lidTarget": self.lid_target, + "lidTemp": self.lid_temp, + "lidTempStatus": self.lid_temp_status, + "currentTemp": self.temperature, + "targetTemp": self.target, + "holdTime": self.hold_time, + "rampRate": self.ramp_rate, + "currentCycleIndex": self.current_cycle_index, + "totalCycleCount": self.total_cycle_count, + "currentStepIndex": self.current_step_index, + "totalStepCount": self.total_step_count, + } return { "status": self.status, - "data": { - "lid": self.lid_status, - "lidTarget": self.lid_target, - "lidTemp": self.lid_temp, - "lidTempStatus": self.lid_temp_status, - "currentTemp": self.temperature, - "targetTemp": self.target, - "holdTime": self.hold_time, - "rampRate": self.ramp_rate, - "currentCycleIndex": self.current_cycle_index, - "totalCycleCount": self.total_cycle_count, - "currentStepIndex": self.current_step_index, - "totalStepCount": self.total_step_count, - }, + "data": data, } @property diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 9b7c33058d4..00924ad50af 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -13,10 +13,16 @@ Optional, cast, TYPE_CHECKING, + TypeGuard, ) from typing_extensions import TypedDict from pathlib import Path +from opentrons.drivers.flex_stacker.types import ( + LimitSwitchStatus, + PlatformStatus, + StackerAxis, +) from opentrons.drivers.rpi_drivers.types import USBPort if TYPE_CHECKING: @@ -27,6 +33,7 @@ HeaterShakerModuleType, MagneticBlockType, AbsorbanceReaderType, + FlexStackerModuleType, ) @@ -50,9 +57,113 @@ class ThermocyclerCycle(TypedDict): ModuleDisconnectedCallback = Optional[Callable[[str, str | None], None]] +class MagneticModuleData(TypedDict): + engaged: bool + height: float + + +class TemperatureModuleData(TypedDict): + currentTemp: float + targetTemp: float | None + + +class HeaterShakerData(TypedDict): + temperatureStatus: str + speedStatus: str + labwareLatchStatus: str + currentTemp: float + targetTemp: float | None + currentSpeed: int + targetSpeed: int | None + errorDetails: str | None + + +class ThermocyclerData(TypedDict): + lid: str + lidTarget: float | None + lidTemp: float + lidTempStatus: str + currentTemp: float | None + targetTemp: float | None + holdTime: float | None + rampRate: float | None + currentCycleIndex: int | None + totalCycleCount: int | None + currentStepIndex: int | None + totalStepCount: int | None + + +class AbsorbanceReaderData(TypedDict): + uptime: int + deviceStatus: str + lidStatus: str + platePresence: str + measureMode: str + sampleWavelengths: List[int] + referenceWavelength: int + + +class FlexStackerData(TypedDict): + latchState: str + platformState: str + hopperDoorState: str + axisStateX: str + axisStateZ: str + errorDetails: str | None + + +ModuleData = Union[ + Dict[Any, Any], # This allows an empty dict as module data + MagneticModuleData, + TemperatureModuleData, + HeaterShakerData, + ThermocyclerData, + AbsorbanceReaderData, + FlexStackerData, +] + + +class ModuleDataValidator: + @classmethod + def is_magnetic_module_data( + cls, data: ModuleData | None + ) -> TypeGuard[MagneticModuleData]: + return data is not None and "engaged" in data.keys() + + @classmethod + def is_temperature_module_data( + cls, data: ModuleData | None + ) -> TypeGuard[TemperatureModuleData]: + return data is not None and "targetTemp" in data.keys() + + @classmethod + def is_heater_shaker_data( + cls, data: ModuleData | None + ) -> TypeGuard[HeaterShakerData]: + return data is not None and "labwareLatchStatus" in data.keys() + + @classmethod + def is_thermocycler_data( + cls, data: ModuleData | None + ) -> TypeGuard[ThermocyclerData]: + return data is not None and "lid" in data.keys() + + @classmethod + def is_absorbance_reader_data( + cls, data: ModuleData | None + ) -> TypeGuard[AbsorbanceReaderData]: + return data is not None and "uptime" in data.keys() + + @classmethod + def is_flex_stacker_data( + cls, data: ModuleData | None + ) -> TypeGuard[FlexStackerData]: + return data is not None and "platformState" in data.keys() + + class LiveData(TypedDict): status: str - data: Dict[str, Union[float, str, bool, List[int], None]] + data: ModuleData | None class ModuleType(str, Enum): @@ -62,6 +173,7 @@ class ModuleType(str, Enum): HEATER_SHAKER: HeaterShakerModuleType = "heaterShakerModuleType" MAGNETIC_BLOCK: MagneticBlockType = "magneticBlockType" ABSORBANCE_READER: AbsorbanceReaderType = "absorbanceReaderType" + FLEX_STACKER: FlexStackerModuleType = "flexStackerModuleType" @classmethod def from_model(cls, model: ModuleModel) -> ModuleType: @@ -77,6 +189,8 @@ def from_model(cls, model: ModuleModel) -> ModuleType: return cls.MAGNETIC_BLOCK if isinstance(model, AbsorbanceReaderModel): return cls.ABSORBANCE_READER + if isinstance(model, FlexStackerModuleModel): + return cls.FLEX_STACKER @classmethod def to_module_fixture_id(cls, module_type: ModuleType) -> str: @@ -91,6 +205,8 @@ def to_module_fixture_id(cls, module_type: ModuleType) -> str: return "magneticBlockV1" if module_type == ModuleType.ABSORBANCE_READER: return "absorbanceReaderV1" + if module_type == ModuleType.FLEX_STACKER: + return "flexStackerModuleV1" else: raise ValueError( f"Module Type {module_type} does not have a related fixture ID." @@ -124,6 +240,10 @@ class AbsorbanceReaderModel(str, Enum): ABSORBANCE_READER_V1: str = "absorbanceReaderV1" +class FlexStackerModuleModel(str, Enum): + FLEX_STACKER_V1: str = "flexStackerModuleV1" + + def module_model_from_string(model_string: str) -> ModuleModel: for model_enum in { MagneticModuleModel, @@ -132,6 +252,7 @@ def module_model_from_string(model_string: str) -> ModuleModel: HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, + FlexStackerModuleModel, }: try: return cast(ModuleModel, model_enum(model_string)) @@ -184,6 +305,7 @@ class ModuleInfo(NamedTuple): HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, + FlexStackerModuleModel, ] @@ -225,3 +347,71 @@ class LidStatus(str, Enum): OFF = "off" UNKNOWN = "unknown" ERROR = "error" + + +class FlexStackerStatus(str, Enum): + IDLE = "idle" + DISPENSING = "dispensing" + STORING = "storing" + ERROR = "error" + + +class PlatformState(str, Enum): + UNKNOWN = "unknown" + EXTENDED = "extended" + RETRACTED = "retracted" + + @classmethod + def from_status(cls, status: PlatformStatus) -> "PlatformState": + """Get the state from the platform status.""" + if status.E and not status.R: + return cls.EXTENDED + if status.R and not status.E: + return cls.RETRACTED + return cls.UNKNOWN + + +class StackerAxisState(str, Enum): + UNKNOWN = "unknown" + EXTENDED = "extended" + RETRACTED = "retracted" + + @classmethod + def from_status( + cls, status: LimitSwitchStatus, axis: StackerAxis + ) -> "StackerAxisState": + """Get the axis state from the limit switch status.""" + match axis: + case StackerAxis.X: + if status.XE and not status.XR: + return cls.EXTENDED + if status.XR and not status.XE: + return cls.RETRACTED + case StackerAxis.Z: + if status.ZE and not status.ZR: + return cls.EXTENDED + if status.ZR and not status.ZE: + return cls.RETRACTED + case StackerAxis.L: + return cls.EXTENDED if status.LR else cls.RETRACTED + return cls.UNKNOWN + + +class LatchState(str, Enum): + CLOSED = "closed" + OPENED = "opened" + + @classmethod + def from_state(cls, state: StackerAxisState) -> "LatchState": + """Get the latch state from the axis state.""" + return cls.CLOSED if state == StackerAxisState.EXTENDED else cls.OPENED + + +class HopperDoorState(str, Enum): + CLOSED = "closed" + OPENED = "opened" + + @classmethod + def from_state(cls, state: bool) -> "HopperDoorState": + """Get the hopper door state from the door state boolean.""" + return cls.CLOSED if state else cls.OPENED diff --git a/api/src/opentrons/hardware_control/modules/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index 296c89a1311..481d0366324 100644 --- a/api/src/opentrons/hardware_control/modules/utils.py +++ b/api/src/opentrons/hardware_control/modules/utils.py @@ -13,6 +13,7 @@ from .thermocycler import Thermocycler from .heater_shaker import HeaterShaker from .absorbance_reader import AbsorbanceReader +from .flex_stacker import FlexStacker log = logging.getLogger(__name__) @@ -26,6 +27,7 @@ Thermocycler.name(): Thermocycler.MODULE_TYPE, HeaterShaker.name(): HeaterShaker.MODULE_TYPE, AbsorbanceReader.name(): AbsorbanceReader.MODULE_TYPE, + FlexStacker.name(): FlexStacker.MODULE_TYPE, } _MODULE_CLS_BY_TYPE: Dict[ModuleType, Type[AbstractModule]] = { @@ -34,6 +36,7 @@ Thermocycler.MODULE_TYPE: Thermocycler, HeaterShaker.MODULE_TYPE: HeaterShaker, AbsorbanceReader.MODULE_TYPE: AbsorbanceReader, + FlexStacker.MODULE_TYPE: FlexStacker, } diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py index dc848cfb7e2..ebb903591ea 100644 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -83,7 +83,10 @@ def _sort_volume_and_values(self) -> None: ) -@dataclass +# We use slots for this dataclass (and the rest of liquid properties) to prevent dynamic creation of attributes +# not defined in the class, not for any performance reasons. This is so that mistyping properties when overriding +# values will cause the protocol to fail analysis, rather than silently passing. +@dataclass(slots=True) class DelayProperties: _enabled: bool @@ -118,7 +121,7 @@ def as_shared_data_model(self) -> SharedDataDelayProperties: ) -@dataclass +@dataclass(slots=True) class TouchTipProperties: _enabled: bool @@ -157,7 +160,7 @@ def mm_to_edge(self) -> Optional[float]: @mm_to_edge.setter def mm_to_edge(self, new_mm: float) -> None: validated_mm = validation.ensure_float(new_mm) - self._z_offset = validated_mm + self._mm_to_edge = validated_mm @property def speed(self) -> Optional[float]: @@ -190,7 +193,7 @@ def as_shared_data_model(self) -> SharedDataTouchTipProperties: ) -@dataclass +@dataclass(slots=True) class MixProperties: _enabled: bool @@ -243,7 +246,7 @@ def as_shared_data_model(self) -> SharedDataMixProperties: ) -@dataclass +@dataclass(slots=True) class BlowoutProperties: _enabled: bool @@ -297,7 +300,7 @@ def as_shared_data_model(self) -> SharedDataBlowoutProperties: ) -@dataclass +@dataclass(slots=True) class SubmergeRetractCommon: _position_reference: PositionReference @@ -336,10 +339,8 @@ def delay(self) -> DelayProperties: return self._delay -@dataclass +@dataclass(slots=True) class Submerge(SubmergeRetractCommon): - ... - def as_shared_data_model(self) -> SharedDataSubmerge: return SharedDataSubmerge( positionReference=self._position_reference, @@ -349,7 +350,7 @@ def as_shared_data_model(self) -> SharedDataSubmerge: ) -@dataclass +@dataclass(slots=True) class RetractAspirate(SubmergeRetractCommon): _air_gap_by_volume: LiquidHandlingPropertyByVolume @@ -374,7 +375,7 @@ def as_shared_data_model(self) -> SharedDataRetractAspirate: ) -@dataclass +@dataclass(slots=True) class RetractDispense(SubmergeRetractCommon): _air_gap_by_volume: LiquidHandlingPropertyByVolume @@ -405,7 +406,7 @@ def as_shared_data_model(self) -> SharedDataRetractDispense: ) -@dataclass +@dataclass(slots=True) class BaseLiquidHandlingProperties: _submerge: Submerge @@ -449,7 +450,7 @@ def delay(self) -> DelayProperties: return self._delay -@dataclass +@dataclass(slots=True) class AspirateProperties(BaseLiquidHandlingProperties): _retract: RetractAspirate @@ -487,7 +488,7 @@ def as_shared_data_model(self) -> SharedDataAspirateProperties: ) -@dataclass +@dataclass(slots=True) class SingleDispenseProperties(BaseLiquidHandlingProperties): _retract: RetractDispense @@ -520,7 +521,7 @@ def as_shared_data_model(self) -> SharedDataSingleDispenseProperties: ) -@dataclass +@dataclass(slots=True) class MultiDispenseProperties(BaseLiquidHandlingProperties): _retract: RetractDispense @@ -553,7 +554,7 @@ def as_shared_data_model(self) -> SharedDataMultiDispenseProperties: ) -@dataclass +@dataclass(slots=True) class TransferProperties: _aspirate: AspirateProperties _dispense: SingleDispenseProperties diff --git a/api/src/opentrons/protocol_api/core/common.py b/api/src/opentrons/protocol_api/core/common.py index 3aff2523a1f..b1eeca1bd56 100644 --- a/api/src/opentrons/protocol_api/core/common.py +++ b/api/src/opentrons/protocol_api/core/common.py @@ -11,6 +11,7 @@ AbstractHeaterShakerCore, AbstractMagneticBlockCore, AbstractAbsorbanceReaderCore, + AbstractFlexStackerCore, ) from .protocol import AbstractProtocol from .well import AbstractWellCore @@ -19,7 +20,7 @@ WellCore = AbstractWellCore LabwareCore = AbstractLabware[WellCore] -InstrumentCore = AbstractInstrument[WellCore] +InstrumentCore = AbstractInstrument[WellCore, LabwareCore] ModuleCore = AbstractModuleCore TemperatureModuleCore = AbstractTemperatureModuleCore MagneticModuleCore = AbstractMagneticModuleCore @@ -27,5 +28,6 @@ HeaterShakerCore = AbstractHeaterShakerCore MagneticBlockCore = AbstractMagneticBlockCore AbsorbanceReaderCore = AbstractAbsorbanceReaderCore +FlexStackerCore = AbstractFlexStackerCore RobotCore = AbstractRobot ProtocolCore = AbstractProtocol[InstrumentCore, LabwareCore, ModuleCore] diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 010f3110fdb..6f9fed991f9 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -2,13 +2,22 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING, cast, Union, List +from typing import ( + Optional, + TYPE_CHECKING, + cast, + Union, + List, + Tuple, + NamedTuple, +) from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 +from opentrons.protocols.advanced_control.transfers import common as tx_commons from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine import ( DeckPoint, @@ -28,29 +37,33 @@ NozzleLayoutConfigurationType, AddressableOffsetVector, LiquidClassRecord, + NextTipInfo, ) from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.types import PipetteNameType, PIPETTE_API_NAMES_MAP from opentrons_shared_data.errors.exceptions import ( UnsupportedHardwareCommand, ) from opentrons.protocol_api._nozzle_layout import NozzleLayout from . import overlap_versions, pipette_movement_conflict +from . import transfer_components_executor as tx_comps_executor from .well import WellCore +from .labware import LabwareCore from ..instrument import AbstractInstrument from ...disposal_locations import TrashBin, WasteChute if TYPE_CHECKING: from .protocol import ProtocolCore from opentrons.protocol_api._liquid import LiquidClass + from opentrons.protocol_api._liquid_properties import TransferProperties _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17) -class InstrumentCore(AbstractInstrument[WellCore]): +class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]): """Instrument API core using a ProtocolEngine. Args: @@ -685,7 +698,7 @@ def get_mount(self) -> Mount: ).mount.to_hw_mount() def get_pipette_name(self) -> str: - """Get the pipette's load name as a string. + """Get the pipette's name as a string. Will match the load name of the actually loaded pipette, which may differ from the requested load name. @@ -699,6 +712,24 @@ def get_pipette_name(self) -> str: else pipette.pipetteName ) + def get_load_name(self) -> str: + """Get the pipette's requested API load name. + + This is the load name that is specified in the `ProtocolContext.load_instrument()` + method. This name might differ from the engine-specific pipette name. + """ + pipette = self._engine_client.state.pipettes.get(self._pipette_id) + load_name = next( + ( + pip_api_name + for pip_api_name, pip_name in PIPETTE_API_NAMES_MAP.items() + if pip_name == pipette.pipetteName + ), + None, + ) + assert load_name, "Load name not found." + return load_name + def get_model(self) -> str: return self._engine_client.state.pipettes.get_model_name(self._pipette_id) @@ -866,23 +897,28 @@ def configure_nozzle_layout( def load_liquid_class( self, - liquid_class: LiquidClass, - pipette_load_name: str, + name: str, + transfer_properties: TransferProperties, tiprack_uri: str, ) -> str: - """Load a liquid class into the engine and return its ID.""" - transfer_props = liquid_class.get_for( - pipette=pipette_load_name, tiprack=tiprack_uri - ) + """Load a liquid class into the engine and return its ID. + Args: + name: Name of the liquid class + transfer_properties: Liquid class properties for a specific pipette & tiprack combination + tiprack_uri: URI of the tiprack whose transfer properties we will be using. + + Returns: + Liquid class record's ID, as generated by the protocol engine. + """ liquid_class_record = LiquidClassRecord( - liquidClassName=liquid_class.name, - pipetteModel=self.get_model(), # TODO: verify this is the correct 'model' to use + liquidClassName=name, + pipetteModel=self.get_load_name(), tiprack=tiprack_uri, - aspirate=transfer_props.aspirate.as_shared_data_model(), - singleDispense=transfer_props.dispense.as_shared_data_model(), - multiDispense=transfer_props.multi_dispense.as_shared_data_model() - if transfer_props.multi_dispense + aspirate=transfer_properties.aspirate.as_shared_data_model(), + singleDispense=transfer_properties.dispense.as_shared_data_model(), + multiDispense=transfer_properties.multi_dispense.as_shared_data_model() + if transfer_properties.multi_dispense else None, ) result = self._engine_client.execute_command_without_recovery( @@ -892,16 +928,351 @@ def load_liquid_class( ) return result.liquidClassId - def transfer_liquid( + def get_next_tip( + self, tip_racks: List[LabwareCore], starting_well: Optional[str] + ) -> Optional[NextTipInfo]: + """Get the next tip to pick up.""" + result = self._engine_client.execute_command_without_recovery( + cmd.GetNextTipParams( + pipetteId=self._pipette_id, + labwareIds=[tip_rack.labware_id for tip_rack in tip_racks], + startingTipWell=starting_well, + ) + ) + return ( + result.nextTipInfo if isinstance(result.nextTipInfo, NextTipInfo) else None + ) + + def transfer_liquid( # noqa: C901 self, - liquid_class_id: str, + liquid_class: LiquidClass, volume: float, - source: List[WellCore], - dest: List[WellCore], + source: List[Tuple[Location, WellCore]], + dest: List[Tuple[Location, WellCore]], new_tip: TransferTipPolicyV2, - trash_location: Union[WellCore, Location, TrashBin, WasteChute], + tip_racks: List[Tuple[Location, LabwareCore]], + trash_location: Union[Location, TrashBin, WasteChute], ) -> None: - """Execute transfer using liquid class properties.""" + """Execute transfer using liquid class properties. + + Args: + liquid_class: The liquid class to use for transfer properties. + volume: Volume to transfer per well. + source: List of source wells, with each well represented as a tuple of + types.Location and WellCore. + types.Location is only necessary for saving the last accessed location. + dest: List of destination wells, with each well represented as a tuple of + types.Location and WellCore. + types.Location is only necessary for saving the last accessed location. + new_tip: Whether the transfer should use a new tip 'once', 'never', 'always', + or 'per source'. + tiprack_uri: The URI of the tiprack that the transfer settings are for. + tip_drop_location: Location where the tip will be dropped (if appropriate). + """ + if not tip_racks: + raise RuntimeError( + "No tipracks found for pipette in order to perform transfer" + ) + tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri() + transfer_props = liquid_class.get_for( + pipette=self.get_load_name(), + tiprack=tiprack_uri_for_transfer_props, + ) + # TODO: use the ID returned by load_liquid_class in command annotations + self.load_liquid_class( + name=liquid_class.name, + transfer_properties=transfer_props, + tiprack_uri=tiprack_uri_for_transfer_props, + ) + + # TODO: add multi-channel pipette handling here + source_dest_per_volume_step = tx_commons.expand_for_volume_constraints( + volumes=[volume for _ in range(len(source))], + targets=zip(source, dest), + max_volume=self.get_max_volume(), + ) + + def _drop_tip() -> None: + if isinstance(trash_location, (TrashBin, WasteChute)): + self.drop_tip_in_disposal_location( + disposal_location=trash_location, + home_after=False, + alternate_tip_drop=True, + ) + elif isinstance(trash_location, Location): + self.drop_tip( + location=trash_location, + well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type] + home_after=False, + alternate_drop_location=True, + ) + + def _pick_up_tip() -> None: + next_tip = self.get_next_tip( + tip_racks=[core for loc, core in tip_racks], + starting_well=None, + ) + if next_tip is None: + raise RuntimeError( + f"No tip available among {tip_racks} for this transfer." + ) + ( + tiprack_loc, + tiprack_uri, + tip_well, + ) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks) + if tiprack_uri != tiprack_uri_for_transfer_props: + raise RuntimeError( + f"Tiprack {tiprack_uri} does not match the tiprack designated " + f"for this transfer- {tiprack_uri_for_transfer_props}." + ) + self.pick_up_tip( + location=tiprack_loc, + well_core=tip_well, + presses=None, + increment=None, + ) + + if new_tip == TransferTipPolicyV2.ONCE: + _pick_up_tip() + + prev_src: Optional[Tuple[Location, WellCore]] = None + post_disp_tip_contents = [ + tx_comps_executor.LiquidAndAirGapPair( + liquid=0, + air_gap=0, + ) + ] + next_step_volume, next_src_dest_combo = next(source_dest_per_volume_step) + is_last_step = False + while not is_last_step: + step_volume = next_step_volume + src_dest_combo = next_src_dest_combo + step_source, step_destination = src_dest_combo + try: + next_step_volume, next_src_dest_combo = next( + source_dest_per_volume_step + ) + except StopIteration: + is_last_step = True + + if new_tip == TransferTipPolicyV2.ALWAYS or ( + new_tip == TransferTipPolicyV2.PER_SOURCE and step_source != prev_src + ): + if prev_src is not None: + _drop_tip() + _pick_up_tip() + post_disp_tip_contents = [ + tx_comps_executor.LiquidAndAirGapPair( + liquid=0, + air_gap=0, + ) + ] + + post_asp_tip_contents = self.aspirate_liquid_class( + volume=step_volume, + source=step_source, + transfer_properties=transfer_props, + transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE, + tip_contents=post_disp_tip_contents, + ) + post_disp_tip_contents = self.dispense_liquid_class( + volume=step_volume, + dest=step_destination, + source=step_source, + transfer_properties=transfer_props, + transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE, + tip_contents=post_asp_tip_contents, + add_final_air_gap=False + if is_last_step and new_tip == TransferTipPolicyV2.NEVER + else True, + trash_location=trash_location, + ) + prev_src = step_source + if new_tip != TransferTipPolicyV2.NEVER: + _drop_tip() + + def _get_location_and_well_core_from_next_tip_info( + self, + tip_info: NextTipInfo, + tip_racks: List[Tuple[Location, LabwareCore]], + ) -> _TipInfo: + tiprack_labware_core = self._protocol_core._labware_cores_by_id[ + tip_info.labwareId + ] + tip_well = tiprack_labware_core.get_well_core(tip_info.tipStartingWell) + + tiprack_loc = [ + loc for loc, lw_core in tip_racks if lw_core == tiprack_labware_core + ] + + return _TipInfo( + Location(tip_well.get_top(0), tiprack_loc[0].labware), + tiprack_labware_core.get_uri(), + tip_well, + ) + + def aspirate_liquid_class( + self, + volume: float, + source: Tuple[Location, WellCore], + transfer_properties: TransferProperties, + transfer_type: tx_comps_executor.TransferType, + tip_contents: List[tx_comps_executor.LiquidAndAirGapPair], + ) -> List[tx_comps_executor.LiquidAndAirGapPair]: + """Execute aspiration steps. + + 1. Submerge + 2. Mix + 3. pre-wet + 4. Aspirate + 5. Delay- wait inside the liquid + 6. Aspirate retract + + Return: List of liquid and air gap pairs in tip. + """ + aspirate_props = transfer_properties.aspirate + tx_commons.check_valid_volume_parameters( + disposal_volume=0, # No disposal volume for 1-to-1 transfer + air_gap=aspirate_props.retract.air_gap_by_volume.get_for_volume(volume), + max_volume=self.get_max_volume(), + ) + source_loc, source_well = source + aspirate_point = ( + tx_comps_executor.absolute_point_from_position_reference_and_offset( + well=source_well, + position_reference=aspirate_props.position_reference, + offset=aspirate_props.offset, + ) + ) + aspirate_location = Location(aspirate_point, labware=source_loc.labware) + last_liquid_and_airgap_in_tip = ( + tip_contents[-1] + if tip_contents + else tx_comps_executor.LiquidAndAirGapPair( + liquid=0, + air_gap=0, + ) + ) + components_executor = tx_comps_executor.TransferComponentsExecutor( + instrument_core=self, + transfer_properties=transfer_properties, + target_location=aspirate_location, + target_well=source_well, + transfer_type=transfer_type, + tip_state=tx_comps_executor.TipState( + last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip + ), + ) + components_executor.submerge(submerge_properties=aspirate_props.submerge) + # TODO: when aspirating for consolidation, do not perform mix + components_executor.mix( + mix_properties=aspirate_props.mix, last_dispense_push_out=False + ) + # TODO: when aspirating for consolidation, do not preform pre-wet + components_executor.pre_wet( + volume=volume, + ) + components_executor.aspirate_and_wait(volume=volume) + components_executor.retract_after_aspiration(volume=volume) + + # return copy of tip_contents with last entry replaced by tip state from executor + last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip + new_tip_contents = tip_contents[0:-1] + [last_contents] + return new_tip_contents + + def dispense_liquid_class( + self, + volume: float, + dest: Tuple[Location, WellCore], + source: Optional[Tuple[Location, WellCore]], + transfer_properties: TransferProperties, + transfer_type: tx_comps_executor.TransferType, + tip_contents: List[tx_comps_executor.LiquidAndAirGapPair], + add_final_air_gap: bool, + trash_location: Union[Location, TrashBin, WasteChute], + ) -> List[tx_comps_executor.LiquidAndAirGapPair]: + """Execute single-dispense steps. + 1. Move pipette to the ‘submerge’ position with normal speed. + - The pipette will move in an arc- move to max z height of labware + (if asp & disp are in same labware) + or max z height of all labware (if asp & disp are in separate labware) + 2. Air gap removal: + - If dispense location is above the meniscus, DO NOT remove air gap + (it will be dispensed along with rest of the liquid later). + All other scenarios, remove the air gap by doing a dispense + - Flow rate = min(dispenseFlowRate, (airGapByVolume)/sec) + - Use the post-dispense delay + 4. Move to the dispense position at the specified ‘submerge’ speed + (even if we might not be moving into the liquid) + - Do a delay (submerge delay) + 6. Dispense: + - Dispense at the specified flow rate. + - Do a push out as specified ONLY IF there is no mix following the dispense AND the tip is empty. + Volume for push out is the volume being dispensed. So if we are dispensing 50uL, use pushOutByVolume[50] as push out volume. + 7. Delay + 8. Mix using the same flow rate and delays as specified for asp+disp, + with the volume and the number of repetitions specified. Use the delays in asp & disp. + - If the dispense position is outside the liquid, then raise error if mix is enabled. + Can only be checked if using liquid level detection/ meniscus-based positioning. + - If the user wants to perform a mix then they should specify a dispense position that’s inside the liquid OR do mix() on the wells after transfer. + - Do push out at the last dispense. + 9. Retract + + Return: + List of liquid and air gap pairs in tip. + """ + dispense_props = transfer_properties.dispense + dest_loc, dest_well = dest + dispense_point = ( + tx_comps_executor.absolute_point_from_position_reference_and_offset( + well=dest_well, + position_reference=dispense_props.position_reference, + offset=dispense_props.offset, + ) + ) + dispense_location = Location(dispense_point, labware=dest_loc.labware) + last_liquid_and_airgap_in_tip = ( + tip_contents[-1] + if tip_contents + else tx_comps_executor.LiquidAndAirGapPair( + liquid=0, + air_gap=0, + ) + ) + components_executor = tx_comps_executor.TransferComponentsExecutor( + instrument_core=self, + transfer_properties=transfer_properties, + target_location=dispense_location, + target_well=dest_well, + transfer_type=transfer_type, + tip_state=tx_comps_executor.TipState( + last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip + ), + ) + components_executor.submerge(submerge_properties=dispense_props.submerge) + if dispense_props.mix.enabled: + push_out_vol = 0.0 + else: + # TODO: if distributing, do a push out only at the last dispense + push_out_vol = dispense_props.push_out_by_volume.get_for_volume(volume) + components_executor.dispense_and_wait( + volume=volume, + push_out_override=push_out_vol, + ) + components_executor.mix( + mix_properties=dispense_props.mix, + last_dispense_push_out=True, + ) + components_executor.retract_after_dispensing( + trash_location=trash_location, + source_location=source[0] if source else None, + source_well=source[1] if source else None, + add_final_air_gap=add_final_air_gap, + ) + last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip + new_tip_contents = tip_contents[0:-1] + [last_contents] + return new_tip_contents def retract(self) -> None: """Retract this instrument to the top of the gantry.""" @@ -994,3 +1365,13 @@ def nozzle_configuration_valid_for_lld(self) -> bool: return self._engine_client.state.pipettes.get_nozzle_configuration_supports_lld( self.pipette_id ) + + def delay(self, seconds: float) -> None: + """Call a protocol delay.""" + self._protocol_core.delay(seconds=seconds, msg=None) + + +class _TipInfo(NamedTuple): + tiprack_location: Location + tiprack_uri: str + tip_well: WellCore diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index d3cf8dca725..c66a1a5459a 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -37,6 +37,7 @@ AbstractHeaterShakerCore, AbstractMagneticBlockCore, AbstractAbsorbanceReaderCore, + AbstractFlexStackerCore, ) from .exceptions import InvalidMagnetEngageHeightError @@ -692,3 +693,25 @@ def is_lid_on(self) -> bool: self.module_id ) return abs_state.is_lid_on + + +class FlexStackerCore(ModuleCore, AbstractFlexStackerCore): + """Flex Stacker core logic implementation for Python protocols.""" + + _sync_module_hardware: SynchronousAdapter[hw_modules.FlexStacker] + + def retrieve(self) -> None: + """Retrieve a labware from the bottom of the Flex Stacker's stack.""" + self._engine_client.execute_command( + cmd.flex_stacker.RetrieveParams( + moduleId=self.module_id, + ) + ) + + def store(self) -> None: + """Store a labware at the bottom of the Flex Stacker's stack.""" + self._engine_client.execute_command( + cmd.flex_stacker.StoreParams( + moduleId=self.module_id, + ) + ) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index bfc808c3091..ece431b0d1e 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -233,6 +233,9 @@ def load_labware( ) # FIXME(jbl, 2023-08-14) validating after loading the object issue validation.ensure_definition_is_labware(load_result.definition) + validation.ensure_definition_is_not_lid_after_api_version( + self.api_version, load_result.definition + ) # FIXME(mm, 2023-02-21): # @@ -322,6 +325,52 @@ def load_adapter( return labware_core + def load_lid( + self, + load_name: str, + location: LabwareCore, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCore: + """Load an individual lid using its identifying parameters. Must be loaded on an existing Labware.""" + load_location = self._convert_labware_location(location=location) + custom_labware_params = ( + self._engine_client.state.labware.find_custom_labware_load_params() + ) + namespace, version = load_labware_params.resolve( + load_name, namespace, version, custom_labware_params + ) + load_result = self._engine_client.execute_command_without_recovery( + cmd.LoadLidParams( + loadName=load_name, + location=load_location, + namespace=namespace, + version=version, + ) + ) + # FIXME(chb, 2024-12-06) validating after loading the object issue + validation.ensure_definition_is_lid(load_result.definition) + + deck_conflict.check( + engine_state=self._engine_client.state, + new_labware_id=load_result.labwareId, + existing_disposal_locations=self._disposal_locations, + # TODO: We can now fetch these IDs from engine too. + # See comment in self.load_labware(). + # + # Wrapping .keys() in list() is just to make Decoy verification easier. + existing_labware_ids=list(self._labware_cores_by_id.keys()), + existing_module_ids=list(self._module_cores_by_id.keys()), + ) + + labware_core = LabwareCore( + labware_id=load_result.labwareId, + engine_client=self._engine_client, + ) + + self._labware_cores_by_id[labware_core.labware_id] = labware_core + return labware_core + def move_labware( self, labware_core: LabwareCore, @@ -644,6 +693,72 @@ def set_last_location( self._last_location = location self._last_mount = mount + def load_lid_stack( + self, + load_name: str, + location: Union[DeckSlotName, StagingSlotName, LabwareCore], + quantity: int, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCore: + """Load a Stack of Lids to a given location, creating a Lid Stack.""" + if quantity < 1: + raise ValueError( + "When loading a lid stack quantity cannot be less than one." + ) + if isinstance(location, DeckSlotName) or isinstance(location, StagingSlotName): + load_location = self._convert_labware_location(location=location) + else: + if isinstance(location, LabwareCore): + load_location = self._convert_labware_location(location=location) + else: + raise ValueError( + "Expected type of Labware Location for lid stack must be Labware, not Legacy Labware or Well." + ) + + custom_labware_params = ( + self._engine_client.state.labware.find_custom_labware_load_params() + ) + namespace, version = load_labware_params.resolve( + load_name, namespace, version, custom_labware_params + ) + + load_result = self._engine_client.execute_command_without_recovery( + cmd.LoadLidStackParams( + loadName=load_name, + location=load_location, + namespace=namespace, + version=version, + quantity=quantity, + ) + ) + + # FIXME(CHB, 2024-12-04) just like load labware and load adapter we have a validating after loading the object issue + validation.ensure_definition_is_lid(load_result.definition) + + deck_conflict.check( + engine_state=self._engine_client.state, + new_labware_id=load_result.stackLabwareId, + existing_disposal_locations=self._disposal_locations, + # TODO (spp, 2023-11-27): We've been using IDs from _labware_cores_by_id + # and _module_cores_by_id instead of getting the lists directly from engine + # because of the chance of engine carrying labware IDs from LPC too. + # But with https://github.com/Opentrons/opentrons/pull/13943, + # & LPC in maintenance runs, we can now rely on engine state for these IDs too. + # Wrapping .keys() in list() is just to make Decoy verification easier. + existing_labware_ids=list(self._labware_cores_by_id.keys()), + existing_module_ids=list(self._module_cores_by_id.keys()), + ) + + labware_core = LabwareCore( + labware_id=load_result.stackLabwareId, + engine_client=self._engine_client, + ) + + self._labware_cores_by_id[labware_core.labware_id] = labware_core + + return labware_core + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" return self._engine_client.state.labware.get_deck_definition() diff --git a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py new file mode 100644 index 00000000000..87e41ee98dc --- /dev/null +++ b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py @@ -0,0 +1,569 @@ +"""Executor for liquid class based complex commands.""" +from __future__ import annotations + +from copy import deepcopy +from enum import Enum +from typing import TYPE_CHECKING, Optional, Union +from dataclasses import dataclass, field + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + PositionReference, + Coordinate, + BlowoutLocation, +) + +from opentrons.protocol_api._liquid_properties import ( + Submerge, + TransferProperties, + MixProperties, +) +from opentrons.protocol_engine.errors import TouchTipDisabledError +from opentrons.types import Location, Point + +if TYPE_CHECKING: + from .well import WellCore + from .instrument import InstrumentCore + from ... import TrashBin, WasteChute + + +@dataclass +class LiquidAndAirGapPair: + """Pairing of a liquid and air gap in a tip, with air gap below the liquid in a tip.""" + + liquid: float = 0 + air_gap: float = 0 + + +@dataclass +class TipState: + """Carrier of the state of the pipette tip in use. + + Properties: + last_liquid_and_air_gap_in_tip: The last liquid + air_gap combo in the tip. + This will only include the existing liquid and air gap in the tip that + an aspirate/ dispense interacts with. For example, the air gap from + a previous step that needs to be removed, or the liquid from a previous + aspirate that needs to be dispensed or the liquid that needs to be added to + during a consolidation. + ready_to_aspirate: Whether the pipette plunger is in a position that allows + correct aspiration. The starting state for the pipette at initialization of + `TransferComponentsExecutor`s should be ready_to_aspirate == True. + """ + + ready_to_aspirate: bool = True + # TODO: maybe use the tip contents from engine state instead. + last_liquid_and_air_gap_in_tip: LiquidAndAirGapPair = field( + default_factory=LiquidAndAirGapPair + ) + + def append_liquid(self, volume: float) -> None: + # Neither aspirate nor a dispense process should be adding liquid + # when there is an air gap present. + assert ( + self.last_liquid_and_air_gap_in_tip.air_gap == 0 + ), "Air gap present in the tip." + self.last_liquid_and_air_gap_in_tip.liquid += volume + + def delete_liquid(self, volume: float) -> None: + # Neither aspirate nor a dispense process should be removing liquid + # when there is an air gap present. + assert ( + self.last_liquid_and_air_gap_in_tip.air_gap == 0 + ), "Air gap present in the tip." + self.last_liquid_and_air_gap_in_tip.liquid -= volume + + def append_air_gap(self, volume: float) -> None: + # Neither aspirate nor a dispense process should be adding air gaps + # when there is already an air gap present. + assert ( + self.last_liquid_and_air_gap_in_tip.air_gap == 0 + ), "Air gap already present in the tip." + self.last_liquid_and_air_gap_in_tip.air_gap = volume + + def delete_air_gap(self, volume: float) -> None: + assert ( + self.last_liquid_and_air_gap_in_tip.air_gap == volume + ), "Last air gap volume doe not match the volume being removed" + self.last_liquid_and_air_gap_in_tip.air_gap = 0 + + +class TransferType(Enum): + ONE_TO_ONE = "one_to_one" + MANY_TO_ONE = "many_to_one" + ONE_TO_MANY = "one_to_many" + + +class TransferComponentsExecutor: + def __init__( + self, + instrument_core: InstrumentCore, + transfer_properties: TransferProperties, + target_location: Location, + target_well: WellCore, + tip_state: TipState, + transfer_type: TransferType, + ) -> None: + self._instrument = instrument_core + self._transfer_properties = transfer_properties + self._target_location = target_location + self._target_well = target_well + self._tip_state: TipState = deepcopy(tip_state) # don't modify caller's object + self._transfer_type: TransferType = transfer_type + + @property + def tip_state(self) -> TipState: + """Return the tip state.""" + return self._tip_state + + def submerge( + self, + submerge_properties: Submerge, + ) -> None: + """Execute submerge steps. + + 1. move to position shown by positionReference + offset (should practically be a point outside/above the liquid). + Should raise an error if this point is inside the liquid? + For liquid meniscus this is easy to tell. Can’t be below meniscus + For reference pos of anything else, do not allow submerge position to be below aspirate position + 2. move to aspirate position at desired speed + 3. delay + """ + # TODO: compare submerge start position and aspirate position and raise error if incompatible + submerge_start_point = absolute_point_from_position_reference_and_offset( + well=self._target_well, + position_reference=submerge_properties.position_reference, + offset=submerge_properties.offset, + ) + submerge_start_location = Location( + point=submerge_start_point, labware=self._target_location.labware + ) + self._instrument.move_to( + location=submerge_start_location, + well_core=self._target_well, + force_direct=False, + minimum_z_height=None, + speed=None, + ) + if self._transfer_type == TransferType.ONE_TO_ONE: + self._remove_air_gap(location=submerge_start_location) + self._instrument.move_to( + location=self._target_location, + well_core=self._target_well, + force_direct=True, + minimum_z_height=None, + speed=submerge_properties.speed, + ) + if submerge_properties.delay.enabled: + assert submerge_properties.delay.duration is not None + self._instrument.delay(submerge_properties.delay.duration) + + def aspirate_and_wait(self, volume: float) -> None: + """Aspirate according to aspirate properties and wait if enabled.""" + # TODO: handle volume correction + aspirate_props = self._transfer_properties.aspirate + self._instrument.aspirate( + location=self._target_location, + well_core=None, + volume=volume, + rate=1, + flow_rate=aspirate_props.flow_rate_by_volume.get_for_volume(volume), + in_place=True, + is_meniscus=None, # TODO: update this once meniscus is implemented + ) + self._tip_state.append_liquid(volume) + delay_props = aspirate_props.delay + if delay_props.enabled: + # Assertion only for mypy purposes + assert delay_props.duration is not None + self._instrument.delay(delay_props.duration) + + def dispense_and_wait( + self, volume: float, push_out_override: Optional[float] + ) -> None: + """Dispense according to dispense properties and wait if enabled.""" + # TODO: handle volume correction + dispense_props = self._transfer_properties.dispense + self._instrument.dispense( + location=self._target_location, + well_core=None, + volume=volume, + rate=1, + flow_rate=dispense_props.flow_rate_by_volume.get_for_volume(volume), + in_place=True, + push_out=push_out_override, + is_meniscus=None, + ) + if push_out_override: + # If a push out was performed, we need to reset the plunger before we can aspirate again + self._tip_state.ready_to_aspirate = False + self._tip_state.delete_liquid(volume) + dispense_delay = dispense_props.delay + if dispense_delay.enabled: + assert dispense_delay.duration is not None + self._instrument.delay(dispense_delay.duration) + + def mix(self, mix_properties: MixProperties, last_dispense_push_out: bool) -> None: + """Execute mix steps. + + 1. Use same flow rates and delays as aspirate and dispense + 2. Do [(aspirate + dispense) x repetitions] at the same position + 3. Do NOT push out at the end of dispense + 4. USE the delay property from aspirate & dispense during mix as well (flow rate and delay are coordinated with each other) + 5. Do not mix during consolidation + NOTE: For most of our built-in definitions, we will keep _mix_ off because it is a very application specific thing. + We should mention in our docs that users should adjust this property according to their application. + """ + if not mix_properties.enabled: + return + # Assertion only for mypy purposes + assert ( + mix_properties.repetitions is not None and mix_properties.volume is not None + ) + push_out_vol = ( + self._transfer_properties.dispense.push_out_by_volume.get_for_volume( + mix_properties.volume + ) + ) + for n in range(mix_properties.repetitions, 0, -1): + self.aspirate_and_wait(volume=mix_properties.volume) + self.dispense_and_wait( + volume=mix_properties.volume, + push_out_override=push_out_vol + if last_dispense_push_out is True and n == 1 + else 0, + ) + + def pre_wet( + self, + volume: float, + ) -> None: + """Do a pre-wet. + + - 1 combo of aspirate + dispense at the same flow rate as specified in asp & disp and the delays in asp & disp + - Use the target volume/ volume we will be aspirating + - No push out + - No pre-wet for consolidation + """ + if not self._transfer_properties.aspirate.pre_wet: + return + mix_props = MixProperties(_enabled=True, _repetitions=1, _volume=volume) + self.mix(mix_properties=mix_props, last_dispense_push_out=False) + + def retract_after_aspiration(self, volume: float) -> None: + """Execute post-aspiration retraction steps. + + 1. Move TO the position reference+offset AT the specified speed + Raise error if retract is below aspirate position or below the meniscus + 2. Delay + 3. Touch tip + - Move to the Z offset position + - Touch tip to the sides at the specified speed (tip moves back to the center as part of touch tip) + - Return back to the retract position + 4. Air gap + - Air gap volume depends on the amount of liquid in the pipette + So if total aspirated volume is 20, use the value for airGapByVolume[20] + Flow rate = min(aspirateFlowRate, (airGapByVolume)/sec) + - Use post-aspirate delay + """ + # TODO: Raise error if retract is below the meniscus + retract_props = self._transfer_properties.aspirate.retract + retract_point = absolute_point_from_position_reference_and_offset( + well=self._target_well, + position_reference=retract_props.position_reference, + offset=retract_props.offset, + ) + retract_location = Location( + retract_point, labware=self._target_location.labware + ) + self._instrument.move_to( + location=retract_location, + well_core=self._target_well, + force_direct=True, + minimum_z_height=None, + speed=retract_props.speed, + ) + retract_delay = retract_props.delay + if retract_delay.enabled: + assert retract_delay.duration is not None + self._instrument.delay(retract_delay.duration) + touch_tip_props = retract_props.touch_tip + if touch_tip_props.enabled: + assert ( + touch_tip_props.speed is not None + and touch_tip_props.z_offset is not None + and touch_tip_props.mm_to_edge is not None + ) + # TODO: update this once touch tip has mmToEdge + self._instrument.touch_tip( + location=retract_location, + well_core=self._target_well, + radius=1, + z_offset=touch_tip_props.z_offset, + speed=touch_tip_props.speed, + ) + self._instrument.move_to( + location=retract_location, + well_core=self._target_well, + force_direct=True, + minimum_z_height=None, + # Full speed because the tip will already be out of the liquid + speed=None, + ) + self._add_air_gap( + air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume( + volume + ) + ) + + def retract_after_dispensing( + self, + trash_location: Union[Location, TrashBin, WasteChute], + source_location: Optional[Location], + source_well: Optional[WellCore], + add_final_air_gap: bool, + ) -> None: + """Execute post-dispense retraction steps. + 1. Position ref+offset is the ending position. Move to this position using specified speed + 2. If blowout is enabled and “destination” + - Do blow-out (at the retract position) + - Leave plunger down + 3. Touch-tip + 4. If not ready-to-aspirate + - Prepare-to-aspirate (at the retract position) + 5. Air-gap (at the retract position) + - This air gap is for preventing any stray droplets from falling while moving the pipette. + It will be performed out of caution even if we just did a blow_out and should *hypothetically* + have no liquid left in the tip. + - This air gap will be removed at the next aspirate. + If this is the last step of the transfer, and we aren't dropping the tip off, + then the air gap will be left as is(?). + 6. If blowout is “source” or “trash” + - Move to location (top of Well) + - Do blow-out (top of well) + - Do touch-tip (?????) (only if it’s in a non-trash location) + - Prepare-to-aspirate (top of well) + - Do air-gap (top of well) + 7. If drop tip, move to drop tip location, drop tip + """ + # TODO: Raise error if retract is below the meniscus + + retract_props = self._transfer_properties.dispense.retract + retract_point = absolute_point_from_position_reference_and_offset( + well=self._target_well, + position_reference=retract_props.position_reference, + offset=retract_props.offset, + ) + retract_location = Location( + retract_point, labware=self._target_location.labware + ) + self._instrument.move_to( + location=retract_location, + well_core=self._target_well, + force_direct=True, + minimum_z_height=None, + speed=retract_props.speed, + ) + retract_delay = retract_props.delay + if retract_delay.enabled: + assert retract_delay.duration is not None + self._instrument.delay(retract_delay.duration) + + blowout_props = retract_props.blowout + if ( + blowout_props.enabled + and blowout_props.location == BlowoutLocation.DESTINATION + ): + assert blowout_props.flow_rate is not None + self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate) + self._instrument.blow_out( + location=retract_location, + well_core=None, + in_place=True, + ) + self._tip_state.ready_to_aspirate = False + is_final_air_gap = ( + blowout_props.enabled + and blowout_props.location == BlowoutLocation.DESTINATION + ) or not blowout_props.enabled + # Regardless of the blowout location, do touch tip and air gap + # when leaving the dispense well. If this will be the final air gap, i.e, + # we won't be moving to a Trash or a Source for Blowout after this air gap, + # then skip the final air gap if we have been told to do so. + self._do_touch_tip_and_air_gap( + location=retract_location, + well=self._target_well, + skip_air_gap=True if is_final_air_gap and not add_final_air_gap else False, + ) + + if ( + blowout_props.enabled + and blowout_props.location != BlowoutLocation.DESTINATION + ): + # TODO: no-op touch tip if touch tip is enabled and blowout is in trash/ reservoir/ any labware with touch-tip disabled + assert blowout_props.flow_rate is not None + self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate) + touch_tip_and_air_gap_location: Optional[Location] + if blowout_props.location == BlowoutLocation.SOURCE: + if source_location is None or source_well is None: + raise RuntimeError( + "Blowout location is 'source' but source location &/or well is not provided." + ) + # TODO: check if we should add a blowout location z-offset in liq class definition + self._instrument.blow_out( + location=Location( + source_well.get_top(0), labware=source_location.labware + ), + well_core=source_well, + in_place=False, + ) + touch_tip_and_air_gap_location = Location( + source_well.get_top(0), labware=source_location.labware + ) + touch_tip_and_air_gap_well = source_well + else: + self._instrument.blow_out( + location=trash_location, + well_core=None, + in_place=False, + ) + touch_tip_and_air_gap_location = ( + trash_location if isinstance(trash_location, Location) else None + ) + touch_tip_and_air_gap_well = ( + # We have already established that trash location of `Location` type + # has its `labware` as `Well` type. + trash_location.labware.as_well()._core # type: ignore[assignment] + if isinstance(trash_location, Location) + else None + ) + last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap + self._tip_state.delete_air_gap(last_air_gap) + self._tip_state.ready_to_aspirate = False + # Do touch tip and air gap again after blowing out into source well or trash + self._do_touch_tip_and_air_gap( + location=touch_tip_and_air_gap_location, + well=touch_tip_and_air_gap_well, + skip_air_gap=not add_final_air_gap, + ) + + def _do_touch_tip_and_air_gap( + self, + location: Optional[Location], + well: Optional[WellCore], + skip_air_gap: bool, + ) -> None: + """Perform touch tip and air gap as part of post-dispense retract.""" + touch_tip_props = self._transfer_properties.dispense.retract.touch_tip + if touch_tip_props.enabled: + assert ( + touch_tip_props.speed is not None + and touch_tip_props.z_offset is not None + and touch_tip_props.mm_to_edge is not None + ) + # TODO: update this once touch tip has mmToEdge + # Also, check that when blow out is a non-dest-well, + # whether the touch tip params from transfer props should be used for + # both dest-well touch tip and non-dest-well touch tip. + if well is not None and location is not None: + try: + self._instrument.touch_tip( + location=location, + well_core=well, + radius=1, + z_offset=touch_tip_props.z_offset, + speed=touch_tip_props.speed, + ) + except TouchTipDisabledError: + # TODO: log a warning + pass + + # Move back to the 'retract' position + self._instrument.move_to( + location=location, + well_core=well, + force_direct=True, + minimum_z_height=None, + # Full speed because the tip will already be out of the liquid + speed=None, + ) + + if self._transfer_type != TransferType.ONE_TO_MANY: + # TODO: check if it is okay to just do `prepare_to_aspirate` unconditionally + if not self._tip_state.ready_to_aspirate: + self._instrument.prepare_to_aspirate() + self._tip_state.ready_to_aspirate = True + if not skip_air_gap: + self._add_air_gap( + air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume( + 0 + ) + ) + + def _add_air_gap(self, air_gap_volume: float) -> None: + """Add an air gap.""" + if air_gap_volume == 0: + return + aspirate_props = self._transfer_properties.aspirate + # The maximum flow rate should be air_gap_volume per second + flow_rate = min( + aspirate_props.flow_rate_by_volume.get_for_volume(air_gap_volume), + air_gap_volume, + ) + self._instrument.air_gap_in_place(volume=air_gap_volume, flow_rate=flow_rate) + delay_props = aspirate_props.delay + if delay_props.enabled: + # Assertion only for mypy purposes + assert delay_props.duration is not None + self._instrument.delay(delay_props.duration) + self._tip_state.append_air_gap(air_gap_volume) + + def _remove_air_gap(self, location: Location) -> None: + """Remove a previously added air gap.""" + last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap + if last_air_gap == 0: + return + + dispense_props = self._transfer_properties.dispense + # The maximum flow rate should be air_gap_volume per second + flow_rate = min( + dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap), + last_air_gap, + ) + self._instrument.dispense( + location=location, + well_core=None, + volume=last_air_gap, + rate=1, + flow_rate=flow_rate, + in_place=True, + is_meniscus=None, + push_out=0, + ) + self._tip_state.delete_air_gap(last_air_gap) + dispense_delay = dispense_props.delay + if dispense_delay.enabled: + assert dispense_delay.duration is not None + self._instrument.delay(dispense_delay.duration) + + +def absolute_point_from_position_reference_and_offset( + well: WellCore, + position_reference: PositionReference, + offset: Coordinate, +) -> Point: + """Return the absolute point, given the well, the position reference and offset.""" + match position_reference: + case PositionReference.WELL_TOP: + reference_point = well.get_top(0) + case PositionReference.WELL_BOTTOM: + reference_point = well.get_bottom(0) + case PositionReference.WELL_CENTER: + reference_point = well.get_center() + case PositionReference.LIQUID_MENISCUS: + raise NotImplementedError( + "Liquid transfer using liquid-meniscus relative positioning" + " is not yet implemented." + ) + case _: + raise ValueError(f"Unknown position reference {position_reference}") + return reference_point + Point(offset.x, offset.y, offset.z) diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index bc1ec3669df..de918405fdc 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import abstractmethod, ABC -from typing import Any, Generic, Optional, TypeVar, Union, List +from typing import Any, Generic, Optional, TypeVar, Union, List, Tuple from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict @@ -13,9 +13,10 @@ from opentrons.protocol_api._liquid import LiquidClass from ..disposal_locations import TrashBin, WasteChute from .well import WellCoreType +from .labware import LabwareCoreType -class AbstractInstrument(ABC, Generic[WellCoreType]): +class AbstractInstrument(ABC, Generic[WellCoreType, LabwareCoreType]): @abstractmethod def get_default_speed(self) -> float: ... @@ -310,28 +311,16 @@ def configure_nozzle_layout( """ ... - @abstractmethod - def load_liquid_class( - self, - liquid_class: LiquidClass, - pipette_load_name: str, - tiprack_uri: str, - ) -> str: - """Load the liquid class properties of given pipette and tiprack into the engine. - - Returns: ID of the liquid class record - """ - ... - @abstractmethod def transfer_liquid( self, - liquid_class_id: str, + liquid_class: LiquidClass, volume: float, - source: List[WellCoreType], - dest: List[WellCoreType], + source: List[Tuple[types.Location, WellCoreType]], + dest: List[Tuple[types.Location, WellCoreType]], new_tip: TransferTipPolicyV2, - trash_location: Union[WellCoreType, types.Location, TrashBin, WasteChute], + tip_racks: List[Tuple[types.Location, LabwareCoreType]], + trash_location: Union[types.Location, TrashBin, WasteChute], ) -> None: """Transfer a liquid from source to dest according to liquid class properties.""" ... @@ -370,4 +359,4 @@ def nozzle_configuration_valid_for_lld(self) -> bool: """Check if the nozzle configuration currently supports LLD.""" -InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any]) +InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any, Any]) diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index d2d25051d49..bbc02702f93 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, Union, List +from typing import TYPE_CHECKING, Optional, Union, List, Tuple from opentrons import types from opentrons.hardware_control import CriticalPoint @@ -25,6 +25,7 @@ from ...disposal_locations import TrashBin, WasteChute from ..instrument import AbstractInstrument from .legacy_well_core import LegacyWellCore +from .legacy_labware_core import LegacyLabwareCore from .legacy_module_core import LegacyThermocyclerCore, LegacyHeaterShakerCore if TYPE_CHECKING: @@ -37,7 +38,7 @@ """In PAPIv2.1 and below, tips are always dropped 10 mm from the bottom of the well.""" -class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore]): +class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]): """Implementation of the InstrumentContext interface.""" def __init__( @@ -556,24 +557,15 @@ def configure_nozzle_layout( """This will never be called because it was added in API 2.16.""" pass - def load_liquid_class( - self, - liquid_class: LiquidClass, - pipette_load_name: str, - tiprack_uri: str, - ) -> str: - """This will never be called because it was added in ..""" - # TODO(spp, 2024-11-20): update the docstring and error to include API version - assert False, "load_liquid_class is not supported in legacy context" - def transfer_liquid( self, - liquid_class_id: str, + liquid_class: LiquidClass, volume: float, - source: List[LegacyWellCore], - dest: List[LegacyWellCore], + source: List[Tuple[types.Location, LegacyWellCore]], + dest: List[Tuple[types.Location, LegacyWellCore]], new_tip: TransferTipPolicyV2, - trash_location: Union[LegacyWellCore, types.Location, TrashBin, WasteChute], + tip_racks: List[Tuple[types.Location, LegacyLabwareCore]], + trash_location: Union[types.Location, TrashBin, WasteChute], ) -> None: """This will never be called because it was added in ..""" # TODO(spp, 2024-11-20): update the docstring and error to include API version diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index d0b95ed82ca..8adadbe1ecf 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -6,7 +6,13 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType -from opentrons.types import DeckSlotName, StagingSlotName, Location, Mount, Point +from opentrons.types import ( + DeckSlotName, + StagingSlotName, + Location, + Mount, + Point, +) from opentrons.util.broker import Broker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules import AbstractModule, ModuleModel, ModuleType @@ -267,6 +273,16 @@ def load_adapter( """Load an adapter using its identifying parameters""" raise APIVersionError(api_element="Loading adapter") + def load_lid( + self, + load_name: str, + location: LegacyLabwareCore, + namespace: Optional[str], + version: Optional[int], + ) -> LegacyLabwareCore: + """Load an individual lid labware using its identifying parameters. Must be loaded on a labware.""" + raise APIVersionError(api_element="Loading lid") + def load_robot(self) -> None: # type: ignore """Load an adapter using its identifying parameters""" raise APIVersionError(api_element="Loading robot") @@ -478,6 +494,17 @@ def set_last_location( self._last_location = location self._last_mount = mount + def load_lid_stack( + self, + load_name: str, + location: Union[DeckSlotName, StagingSlotName, LegacyLabwareCore], + quantity: int, + namespace: Optional[str], + version: Optional[int], + ) -> LegacyLabwareCore: + """Load a Stack of Lids to a given location, creating a Lid Stack.""" + raise APIVersionError(api_element="Lid stack") + def get_module_cores(self) -> List[legacy_module_core.LegacyModuleCore]: """Get loaded module cores.""" return self._module_cores diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index ec194874528..3a898c27a42 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, Union, List +from typing import TYPE_CHECKING, Optional, Union, List, Tuple from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict @@ -23,6 +23,7 @@ UnexpectedTipAttachError, ) +from ..legacy.legacy_labware_core import LegacyLabwareCore from ...disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.protocol_api._liquid import LiquidClass @@ -42,7 +43,9 @@ """In PAPIv2.1 and below, tips are always dropped 10 mm from the bottom of the well.""" -class LegacyInstrumentCoreSimulator(AbstractInstrument[LegacyWellCore]): +class LegacyInstrumentCoreSimulator( + AbstractInstrument[LegacyWellCore, LegacyLabwareCore] +): """A simulation of an instrument context.""" def __init__( @@ -474,24 +477,15 @@ def configure_nozzle_layout( """This will never be called because it was added in API 2.15.""" pass - def load_liquid_class( - self, - liquid_class: LiquidClass, - pipette_load_name: str, - tiprack_uri: str, - ) -> str: - """This will never be called because it was added in ..""" - # TODO(spp, 2024-11-20): update the docstring and error to include API version - assert False, "load_liquid_class is not supported in legacy context" - def transfer_liquid( self, - liquid_class_id: str, + liquid_class: LiquidClass, volume: float, - source: List[LegacyWellCore], - dest: List[LegacyWellCore], + source: List[Tuple[types.Location, LegacyWellCore]], + dest: List[Tuple[types.Location, LegacyWellCore]], new_tip: TransferTipPolicyV2, - trash_location: Union[LegacyWellCore, types.Location, TrashBin, WasteChute], + tip_racks: List[Tuple[types.Location, LegacyLabwareCore]], + trash_location: Union[types.Location, TrashBin, WasteChute], ) -> None: """Transfer a liquid from source to dest according to liquid class properties.""" # TODO(spp, 2024-11-20): update the docstring and error to include API version diff --git a/api/src/opentrons/protocol_api/core/module.py b/api/src/opentrons/protocol_api/core/module.py index e24fbbc54b0..d2583c711cb 100644 --- a/api/src/opentrons/protocol_api/core/module.py +++ b/api/src/opentrons/protocol_api/core/module.py @@ -379,3 +379,22 @@ def open_lid(self) -> None: @abstractmethod def is_lid_on(self) -> bool: """Return True if the Absorbance Reader's lid is currently closed.""" + + +class AbstractFlexStackerCore(AbstractModuleCore): + """Core control interface for an attached Flex Stacker.""" + + MODULE_TYPE: ClassVar = ModuleType.FLEX_STACKER + + @abstractmethod + def get_serial_number(self) -> str: + """Get the module's unique hardware serial number.""" + + @abstractmethod + def retrieve(self) -> None: + """Release and return a labware at the bottom of the labware stack.""" + + @abstractmethod + def store(self) -> None: + """Store a labware at the bottom of the labware stack.""" + pass diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index ba9f9a7d14a..27d41b921b0 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -10,7 +10,13 @@ from opentrons_shared_data.labware.types import LabwareDefinition from opentrons_shared_data.robot.types import RobotType -from opentrons.types import DeckSlotName, StagingSlotName, Location, Mount, Point +from opentrons.types import ( + DeckSlotName, + StagingSlotName, + Location, + Mount, + Point, +) from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules.types import ModuleModel from opentrons.protocols.api_support.util import AxisMaxSpeeds @@ -94,6 +100,17 @@ def load_adapter( """Load an adapter using its identifying parameters""" ... + @abstractmethod + def load_lid( + self, + load_name: str, + location: LabwareCoreType, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCoreType: + """Load an individual lid labware using its identifying parameters. Must be loaded on a labware.""" + ... + @abstractmethod def move_labware( self, @@ -191,6 +208,17 @@ def set_last_location( ) -> None: ... + @abstractmethod + def load_lid_stack( + self, + load_name: str, + location: Union[DeckSlotName, StagingSlotName, LabwareCoreType], + quantity: int, + namespace: Optional[str], + version: Optional[int], + ) -> LabwareCoreType: + ... + @abstractmethod def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 9c6338270c7..b0f0f666e74 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1520,9 +1520,9 @@ def transfer_liquid( labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] ], new_tip: TransferTipPolicyV2Type = "once", - tip_drop_location: Optional[ + trash_location: Optional[ Union[types.Location, labware.Well, TrashBin, WasteChute] - ] = None, # Maybe call this 'tip_drop_location' which is similar to PD + ] = None, ) -> InstrumentContext: """Transfer liquid from source to dest using the specified liquid class properties. @@ -1561,14 +1561,9 @@ def transfer_liquid( " of 'once' or 'always'." ) else: - tiprack = self._last_tip_picked_up_from.parent + tip_racks = [self._last_tip_picked_up_from.parent] else: - tiprack, well = labware.next_available_tip( - starting_tip=self.starting_tip, - tip_racks=self.tip_racks, - channels=self.active_channels, - nozzle_map=self._core.get_nozzle_map(), - ) + tip_racks = self._tip_racks if self.current_volume != 0: raise RuntimeError( "A transfer on a liquid class cannot start with liquid already in the tip." @@ -1577,39 +1572,36 @@ def transfer_liquid( ) _trash_location: Union[types.Location, labware.Well, TrashBin, WasteChute] - if tip_drop_location is None: + if trash_location is None: saved_trash = self.trash_container if isinstance(saved_trash, labware.Labware): _trash_location = saved_trash.wells()[0] else: _trash_location = saved_trash else: - _trash_location = tip_drop_location + _trash_location = trash_location - checked_trash_location = ( - validation.ensure_valid_tip_drop_location_for_transfer_v2( - tip_drop_location=_trash_location - ) + checked_trash_location = validation.ensure_valid_trash_location_for_transfer_v2( + trash_location=_trash_location ) - liquid_class_id = self._core.load_liquid_class( - liquid_class=liquid_class, - pipette_load_name=self.name, - tiprack_uri=tiprack.uri, - ) - self._core.transfer_liquid( - liquid_class_id=liquid_class_id, + liquid_class=liquid_class, volume=volume, - source=[well._core for well in flat_sources_list], - dest=[well._core for well in flat_dests_list], + source=[ + (types.Location(types.Point(), labware=well), well._core) + for well in flat_sources_list + ], + dest=[ + (types.Location(types.Point(), labware=well), well._core) + for well in flat_dests_list + ], new_tip=valid_new_tip, - trash_location=( - checked_trash_location._core - if isinstance(checked_trash_location, labware.Well) - else checked_trash_location - ), + tip_racks=[ + (types.Location(types.Point(), labware=rack), rack._core) + for rack in tip_racks + ], + trash_location=checked_trash_location, ) - return self @requires_version(2, 0) diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 5e919a44f86..bb8a094e4c2 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -544,6 +544,7 @@ def load_labware( self, name: str, label: Optional[str] = None, + lid: Optional[str] = None, namespace: Optional[str] = None, version: Optional[int] = None, ) -> Labware: @@ -573,6 +574,20 @@ def load_labware( self._core_map.add(labware_core, labware) + if lid is not None: + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid on a Labware", + until_version="2.23", + current_version=f"{self._api_version}", + ) + self._protocol_core.load_lid( + load_name=lid, + location=labware_core, + namespace=namespace, + version=version, + ) + return labware @requires_version(2, 15) @@ -597,6 +612,65 @@ def load_labware_from_definition( label=label, ) + @requires_version(2, 23) + def load_lid_stack( + self, + load_name: str, + quantity: int, + namespace: Optional[str] = None, + version: Optional[int] = None, + ) -> Labware: + """ + Load a stack of Lids onto a valid Deck Location or Adapter. + + :param str load_name: A string to use for looking up a lid definition. + You can find the ``load_name`` for any standard lid on the Opentrons + `Labware Library `_. + :param int quantity: The quantity of lids to be loaded in the stack. + :param str namespace: The namespace that the lid labware definition belongs to. + If unspecified, the API will automatically search two namespaces: + + - ``"opentrons"``, to load standard Opentrons labware definitions. + - ``"custom_beta"``, to load custom labware definitions created with the + `Custom Labware Creator `__. + + You might need to specify an explicit ``namespace`` if you have a custom + definition whose ``load_name`` is the same as an Opentrons-verified + definition, and you want to explicitly choose one or the other. + + :param version: The version of the labware definition. You should normally + leave this unspecified to let ``load_lid_stack()`` choose a version + automatically. + + :return: The initialized and loaded labware object representing the Lid Stack. + """ + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid Stack", + until_version="2.23", + current_version=f"{self._api_version}", + ) + + load_location = self._core + + load_name = validation.ensure_lowercase_name(load_name) + + result = self._protocol_core.load_lid_stack( + load_name=load_name, + location=load_location, + quantity=quantity, + namespace=namespace, + version=version, + ) + + labware = Labware( + core=result, + api_version=self._api_version, + protocol_core=self._protocol_core, + core_map=self._core_map, + ) + return labware + def set_calibration(self, delta: Point) -> None: """ An internal, deprecated method used for updating the labware offset. diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 96950b927ef..82487196e42 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -29,6 +29,7 @@ HeaterShakerCore, MagneticBlockCore, AbsorbanceReaderCore, + FlexStackerCore, ) from .core.core_map import LoadedCoreMap from .core.engine import ENGINE_CORE_API_VERSION @@ -125,6 +126,7 @@ def load_labware( namespace: Optional[str] = None, version: Optional[int] = None, adapter: Optional[str] = None, + lid: Optional[str] = None, ) -> Labware: """Load a labware onto the module using its load parameters. @@ -180,6 +182,19 @@ def load_labware( version=version, location=load_location, ) + if lid is not None: + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a lid on a Labware", + until_version="2.23", + current_version=f"{self._api_version}", + ) + self._protocol_core.load_lid( + load_name=lid, + location=labware_core, + namespace=namespace, + version=version, + ) if isinstance(self._core, LegacyModuleCore): labware = self._core.add_labware_core(cast(LegacyLabwareCore, labware_core)) @@ -1084,3 +1099,34 @@ def read( :returns: A dictionary of wavelengths to dictionary of values ordered by well name. """ return self._core.read(filename=export_filename) + + +class FlexStackerContext(ModuleContext): + """An object representing a connected Flex Stacker module. + + It should not be instantiated directly; instead, it should be + created through :py:meth:`.ProtocolContext.load_module`. + + .. versionadded:: 2.23 + """ + + _core: FlexStackerCore + + @property + @requires_version(2, 23) + def serial_number(self) -> str: + """Get the module's unique hardware serial number.""" + return self._core.get_serial_number() + + @requires_version(2, 23) + def retrieve(self) -> None: + """Release and return a labware at the bottom of the labware stack.""" + self._core.retrieve() + + @requires_version(2, 23) + def store(self, labware: Labware) -> None: + """Store a labware at the bottom of the labware stack. + + :param labware: The labware object to store. + """ + self._core.store() diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 182019674a5..b9f96e4d536 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -399,6 +399,7 @@ def load_labware( namespace: Optional[str] = None, version: Optional[int] = None, adapter: Optional[str] = None, + lid: Optional[str] = None, ) -> Labware: """Load a labware onto a location. @@ -443,6 +444,10 @@ def load_labware( values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The adapter will use the same namespace as the labware, and the API will choose the adapter's version automatically. + :param lid: A lid to load the on top of the main labware. Accepts the same + values as the ``load_name`` parameter of :py:meth:`.load_lid_stack`. The + lid will use the same namespace as the labware, and the API will + choose the lid's version automatically. .. versionadded:: 2.15 """ @@ -483,6 +488,20 @@ def load_labware( version=version, ) + if lid is not None: + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid on a Labware", + until_version="2.23", + current_version=f"{self._api_version}", + ) + self._core.load_lid( + load_name=lid, + location=labware_core, + namespace=namespace, + version=version, + ) + labware = Labware( core=labware_core, api_version=self._api_version, @@ -1334,6 +1353,94 @@ def door_closed(self) -> bool: """Returns ``True`` if the front door of the robot is closed.""" return self._core.door_closed() + @requires_version(2, 23) + def load_lid_stack( + self, + load_name: str, + location: Union[DeckLocation, Labware], + quantity: int, + adapter: Optional[str] = None, + namespace: Optional[str] = None, + version: Optional[int] = None, + ) -> Labware: + """ + Load a stack of Lids onto a valid Deck Location or Adapter. + + :param str load_name: A string to use for looking up a lid definition. + You can find the ``load_name`` for any standard lid on the Opentrons + `Labware Library `_. + :param location: Either a :ref:`deck slot `, + like ``1``, ``"1"``, or ``"D1"``, or the a valid Opentrons Adapter. + :param int quantity: The quantity of lids to be loaded in the stack. + :param adapter: An adapter to load the lid stack on top of. Accepts the same + values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The + adapter will use the same namespace as the lid labware, and the API will + choose the adapter's version automatically. + :param str namespace: The namespace that the lid labware definition belongs to. + If unspecified, the API will automatically search two namespaces: + + - ``"opentrons"``, to load standard Opentrons labware definitions. + - ``"custom_beta"``, to load custom labware definitions created with the + `Custom Labware Creator `__. + + You might need to specify an explicit ``namespace`` if you have a custom + definition whose ``load_name`` is the same as an Opentrons-verified + definition, and you want to explicitly choose one or the other. + + :param version: The version of the labware definition. You should normally + leave this unspecified to let ``load_lid_stack()`` choose a version + automatically. + + :return: The initialized and loaded labware object representing the Lid Stack. + """ + if self._api_version < validation.LID_STACK_VERSION_GATE: + raise APIVersionError( + api_element="Loading a Lid Stack", + until_version="2.23", + current_version=f"{self._api_version}", + ) + + load_location: Union[DeckSlotName, StagingSlotName, LabwareCore] + if isinstance(location, Labware): + load_location = location._core + else: + load_location = validation.ensure_and_convert_deck_slot( + location, self._api_version, self._core.robot_type + ) + + if adapter is not None: + if isinstance(load_location, DeckSlotName) or isinstance( + load_location, StagingSlotName + ): + loaded_adapter = self.load_adapter( + load_name=adapter, + location=load_location.value, + namespace=namespace, + ) + load_location = loaded_adapter._core + else: + raise ValueError( + "Location cannot be a Labware or Adapter when the 'adapter' field is not None." + ) + + load_name = validation.ensure_lowercase_name(load_name) + + result = self._core.load_lid_stack( + load_name=load_name, + location=load_location, + quantity=quantity, + namespace=namespace, + version=version, + ) + + labware = Labware( + core=result, + api_version=self._api_version, + protocol_core=self._core, + core_map=self._core_map, + ) + return labware + def _create_module_context( module_core: Union[ModuleCore, NonConnectedModuleCore], diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index cd1e5112718..fde986c3552 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -14,13 +14,15 @@ from math import isinf, isnan from typing_extensions import TypeGuard -from opentrons_shared_data.labware.labware_definition import LabwareRole -from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + LabwareRole, +) +from opentrons_shared_data.pipette.types import PipetteNameType, PIPETTE_API_NAMES_MAP from opentrons_shared_data.robot.types import RobotType from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import APIVersionError -from opentrons.protocols.models import LabwareDefinition from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.types import ( Mount, @@ -53,29 +55,8 @@ # The first APIVersion where Python protocols can specify staging deck slots (e.g. "D4") _STAGING_DECK_SLOT_VERSION_GATE = APIVersion(2, 16) -# Mapping of public Python Protocol API pipette load names -# to names used by the internal Opentrons system -_PIPETTE_NAMES_MAP = { - "p10_single": PipetteNameType.P10_SINGLE, - "p10_multi": PipetteNameType.P10_MULTI, - "p20_single_gen2": PipetteNameType.P20_SINGLE_GEN2, - "p20_multi_gen2": PipetteNameType.P20_MULTI_GEN2, - "p50_single": PipetteNameType.P50_SINGLE, - "p50_multi": PipetteNameType.P50_MULTI, - "p300_single": PipetteNameType.P300_SINGLE, - "p300_multi": PipetteNameType.P300_MULTI, - "p300_single_gen2": PipetteNameType.P300_SINGLE_GEN2, - "p300_multi_gen2": PipetteNameType.P300_MULTI_GEN2, - "p1000_single": PipetteNameType.P1000_SINGLE, - "p1000_single_gen2": PipetteNameType.P1000_SINGLE_GEN2, - "flex_1channel_50": PipetteNameType.P50_SINGLE_FLEX, - "flex_8channel_50": PipetteNameType.P50_MULTI_FLEX, - "flex_1channel_1000": PipetteNameType.P1000_SINGLE_FLEX, - "flex_8channel_1000": PipetteNameType.P1000_MULTI_FLEX, - "flex_8channel_1000_em": PipetteNameType.P1000_MULTI_EM, - "flex_96channel_1000": PipetteNameType.P1000_96, - "flex_96channel_200": PipetteNameType.P200_96, -} +# The first APIVersion where Python protocols can load lids as stacks and treat them as attributes of a parent labware. +LID_STACK_VERSION_GATE = APIVersion(2, 23) class InvalidPipetteMountError(ValueError): @@ -189,7 +170,7 @@ def ensure_pipette_name(pipette_name: str) -> PipetteNameType: pipette_name = ensure_lowercase_name(pipette_name) try: - return _PIPETTE_NAMES_MAP[pipette_name] + return PIPETTE_API_NAMES_MAP[pipette_name] except KeyError: raise ValueError( f"Cannot resolve {pipette_name} to pipette, must be given valid pipette name." @@ -364,6 +345,27 @@ def ensure_definition_is_labware(definition: LabwareDefinition) -> None: ) +def ensure_definition_is_lid(definition: LabwareDefinition) -> None: + """Ensure that one of the definition's allowed roles is `lid` or that that field is empty.""" + if LabwareRole.lid not in definition.allowedRoles: + raise LabwareDefinitionIsNotLabwareError( + f"Labware {definition.parameters.loadName} is not a lid." + ) + + +def ensure_definition_is_not_lid_after_api_version( + api_version: APIVersion, definition: LabwareDefinition +) -> None: + """Ensure that one of the definition's allowed roles is not `lid` or that the API Version is below the release where lid loading was seperated.""" + if ( + LabwareRole.lid in definition.allowedRoles + and api_version >= LID_STACK_VERSION_GATE + ): + raise APIVersionError( + f"Labware Lids cannot be loaded like standard labware in Protocols written with an API version greater than {LID_STACK_VERSION_GATE}." + ) + + _MODULE_ALIASES: Dict[str, ModuleModel] = { "magdeck": MagneticModuleModel.MAGNETIC_V1, "magnetic module": MagneticModuleModel.MAGNETIC_V1, @@ -687,20 +689,18 @@ def ensure_valid_flat_wells_list_for_transfer_v2( ) -def ensure_valid_tip_drop_location_for_transfer_v2( - tip_drop_location: Union[Location, Well, TrashBin, WasteChute] -) -> Union[Location, Well, TrashBin, WasteChute]: - """Ensure that the tip drop location is valid for v2 transfer.""" +def ensure_valid_trash_location_for_transfer_v2( + trash_location: Union[Location, Well, TrashBin, WasteChute] +) -> Union[Location, TrashBin, WasteChute]: + """Ensure that the trash location is valid for v2 transfer.""" from .labware import Well - if ( - isinstance(tip_drop_location, Well) - or isinstance(tip_drop_location, TrashBin) - or isinstance(tip_drop_location, WasteChute) - ): - return tip_drop_location - elif isinstance(tip_drop_location, Location): - _, maybe_well = tip_drop_location.labware.get_parent_labware_and_well() + if isinstance(trash_location, TrashBin) or isinstance(trash_location, WasteChute): + return trash_location + elif isinstance(trash_location, Well): + return trash_location.top() + elif isinstance(trash_location, Location): + _, maybe_well = trash_location.labware.get_parent_labware_and_well() if maybe_well is None: raise TypeError( @@ -710,11 +710,11 @@ def ensure_valid_tip_drop_location_for_transfer_v2( " since that is where a tip is dropped." " However, the given location doesn't refer to any well." ) - return tip_drop_location + return trash_location else: raise TypeError( f"If specified, location should be an instance of" f" `types.Location` (e.g. the return value from `Well.top()`)" f" or `Well` (e.g. `reservoir.wells()[0]`) or an instance of `TrashBin` or `WasteChute`." - f" However, it is '{tip_drop_location}'." + f" However, it is '{trash_location}'." ) diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index a9dcc3e7dc3..0ec505d68e6 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -8,12 +8,12 @@ from enum import Enum from typing import List, Optional, Union -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.errors import EnumeratedError +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from opentrons.hardware_control.types import DoorState from opentrons.hardware_control.modules import LiveData -from opentrons_shared_data.errors import EnumeratedError - from ..commands import ( Command, CommandCreate, diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 71837a7a2ca..3f7da174750 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -77,6 +77,18 @@ def execute_command_without_recovery( ) -> commands.LoadPipetteResult: pass + @overload + def execute_command_without_recovery( + self, params: commands.LoadLidStackParams + ) -> commands.LoadLidStackResult: + pass + + @overload + def execute_command_without_recovery( + self, params: commands.LoadLidParams + ) -> commands.LoadLidResult: + pass + @overload def execute_command_without_recovery( self, params: commands.LiquidProbeParams @@ -95,6 +107,12 @@ def execute_command_without_recovery( ) -> commands.LoadLiquidClassResult: pass + @overload + def execute_command_without_recovery( + self, params: commands.GetNextTipParams + ) -> commands.GetNextTipResult: + pass + def execute_command_without_recovery( self, params: commands.CommandParams ) -> commands.CommandResult: diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index b5edda52397..a11222f57b0 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -14,6 +14,7 @@ """ from . import absorbance_reader +from . import flex_stacker from . import heater_shaker from . import magnetic_module from . import temperature_module @@ -174,6 +175,22 @@ LoadPipetteCommandType, ) +from .load_lid_stack import ( + LoadLidStack, + LoadLidStackParams, + LoadLidStackCreate, + LoadLidStackResult, + LoadLidStackCommandType, +) + +from .load_lid import ( + LoadLid, + LoadLidParams, + LoadLidCreate, + LoadLidResult, + LoadLidCommandType, +) + from .move_labware import ( MoveLabware, MoveLabwareParams, @@ -476,6 +493,20 @@ "LoadPipetteResult", "LoadPipetteCommandType", "LoadPipettePrivateResult", + # load lid stack command models + "LoadLidStack", + "LoadLidStackCreate", + "LoadLidStackParams", + "LoadLidStackResult", + "LoadLidStackCommandType", + "LoadLidStackPrivateResult", + # load lid command models + "LoadLid", + "LoadLidCreate", + "LoadLidParams", + "LoadLidResult", + "LoadLidCommandType", + "LoadLidPrivateResult", # move labware command models "MoveLabware", "MoveLabwareCreate", @@ -584,6 +615,7 @@ # hardware control command models # hardware module command bundles "absorbance_reader", + "flex_stacker", "heater_shaker", "magnetic_module", "temperature_module", diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py index 14c1f0f9ea3..41c1e65bce9 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py @@ -60,7 +60,6 @@ async def execute(self, params: CloseLidParams) -> SuccessData[CloseLidResult]: hardware_lid_status = AbsorbanceReaderLidStatus.OFF if not self._state_view.config.use_virtual_modules: abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id) - if abs_reader is not None: hardware_lid_status = await abs_reader.get_current_lid_status() else: diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py index 43937c07ab2..bcb2639d0a5 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py @@ -12,6 +12,7 @@ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence from ...errors import InvalidWavelengthError +from ...state import update_types if TYPE_CHECKING: from opentrons.protocol_engine.state.state import StateView @@ -60,6 +61,7 @@ def __init__( async def execute(self, params: InitializeParams) -> SuccessData[InitializeResult]: """Initiate a single absorbance measurement.""" + state_update = update_types.StateUpdate() abs_reader_substate = self._state_view.modules.get_absorbance_reader_substate( module_id=params.moduleId ) @@ -120,10 +122,15 @@ async def execute(self, params: InitializeParams) -> SuccessData[InitializeResul reference_wavelength=params.referenceWavelength, ) - return SuccessData( - public=InitializeResult(), + state_update.initialize_absorbance_reader( + abs_reader_substate.module_id, + params.measureMode, + params.sampleWavelengths, + params.referenceWavelength, ) + return SuccessData(public=InitializeResult(), state_update=state_update) + class Initialize(BaseCommand[InitializeParams, InitializeResult, ErrorOccurrence]): """A command to initialize an Absorbance Reader.""" diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index b06a2527cc8..c8f7dca8706 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -74,6 +74,7 @@ async def execute( # noqa: C901 self, params: ReadAbsorbanceParams ) -> SuccessData[ReadAbsorbanceResult]: """Initiate an absorbance measurement.""" + state_update = update_types.StateUpdate() abs_reader_substate = self._state_view.modules.get_absorbance_reader_substate( module_id=params.moduleId ) @@ -149,6 +150,9 @@ async def execute( # noqa: C901 "Plate Reader data cannot be requested with a module that has not been initialized." ) + state_update.set_absorbance_reader_data( + module_id=abs_reader_substate.module_id, read_result=asbsorbance_result + ) # TODO (cb, 10-17-2024): FILE PROVIDER - Some day we may want to break the file provider behavior into a seperate API function. # When this happens, we probably will to have the change the command results handler we utilize to track file IDs in engine. # Today, the action handler for the FileStore looks for a ReadAbsorbanceResult command action, this will need to be delinked. @@ -181,18 +185,20 @@ async def execute( # noqa: C901 # Return success data to api return SuccessData( public=ReadAbsorbanceResult( - data=asbsorbance_result, fileIds=file_ids + data=asbsorbance_result, + fileIds=file_ids, ), + state_update=state_update, ) + state_update.files_added = update_types.FilesAddedUpdate(file_ids=file_ids) + return SuccessData( public=ReadAbsorbanceResult( data=asbsorbance_result, fileIds=file_ids, ), - state_update=update_types.StateUpdate( - files_added=update_types.FilesAddedUpdate(file_ids=file_ids) - ), + state_update=state_update, ) diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 3f5bb09e510..06a2160c75e 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -16,6 +16,7 @@ from .movement_common import StallOrCollisionError from . import absorbance_reader +from . import flex_stacker from . import heater_shaker from . import magnetic_module from . import temperature_module @@ -161,6 +162,22 @@ LoadPipetteCommandType, ) +from .load_lid_stack import ( + LoadLidStack, + LoadLidStackParams, + LoadLidStackCreate, + LoadLidStackResult, + LoadLidStackCommandType, +) + +from .load_lid import ( + LoadLid, + LoadLidParams, + LoadLidCreate, + LoadLidResult, + LoadLidCommandType, +) + from .move_labware import ( GripperMovementError, MoveLabware, @@ -367,6 +384,8 @@ LoadLiquidClass, LoadModule, LoadPipette, + LoadLidStack, + LoadLid, MoveLabware, MoveRelative, MoveToCoordinates, @@ -412,6 +431,8 @@ absorbance_reader.OpenLid, absorbance_reader.Initialize, absorbance_reader.ReadAbsorbance, + flex_stacker.Retrieve, + flex_stacker.Store, calibration.CalibrateGripper, calibration.CalibratePipette, calibration.CalibrateModule, @@ -448,6 +469,8 @@ HomeParams, RetractAxisParams, LoadLabwareParams, + LoadLidStackParams, + LoadLidParams, ReloadLabwareParams, LoadLiquidParams, LoadLiquidClassParams, @@ -498,6 +521,8 @@ absorbance_reader.OpenLidParams, absorbance_reader.InitializeParams, absorbance_reader.ReadAbsorbanceParams, + flex_stacker.RetrieveParams, + flex_stacker.StoreParams, calibration.CalibrateGripperParams, calibration.CalibratePipetteParams, calibration.CalibrateModuleParams, @@ -537,6 +562,8 @@ LoadLiquidClassCommandType, LoadModuleCommandType, LoadPipetteCommandType, + LoadLidStackCommandType, + LoadLidCommandType, MoveLabwareCommandType, MoveRelativeCommandType, MoveToCoordinatesCommandType, @@ -582,6 +609,8 @@ absorbance_reader.OpenLidCommandType, absorbance_reader.InitializeCommandType, absorbance_reader.ReadAbsorbanceCommandType, + flex_stacker.RetrieveCommandType, + flex_stacker.StoreCommandType, calibration.CalibrateGripperCommandType, calibration.CalibratePipetteCommandType, calibration.CalibrateModuleCommandType, @@ -622,6 +651,8 @@ LoadLiquidClassCreate, LoadModuleCreate, LoadPipetteCreate, + LoadLidStackCreate, + LoadLidCreate, MoveLabwareCreate, MoveRelativeCreate, MoveToCoordinatesCreate, @@ -667,6 +698,8 @@ absorbance_reader.OpenLidCreate, absorbance_reader.InitializeCreate, absorbance_reader.ReadAbsorbanceCreate, + flex_stacker.RetrieveCreate, + flex_stacker.StoreCreate, calibration.CalibrateGripperCreate, calibration.CalibratePipetteCreate, calibration.CalibrateModuleCreate, @@ -715,6 +748,8 @@ LoadLiquidClassResult, LoadModuleResult, LoadPipetteResult, + LoadLidStackResult, + LoadLidResult, MoveLabwareResult, MoveRelativeResult, MoveToCoordinatesResult, @@ -760,6 +795,8 @@ absorbance_reader.OpenLidResult, absorbance_reader.InitializeResult, absorbance_reader.ReadAbsorbanceResult, + flex_stacker.RetrieveResult, + flex_stacker.StoreResult, calibration.CalibrateGripperResult, calibration.CalibratePipetteResult, calibration.CalibrateModuleResult, diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/__init__.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/__init__.py new file mode 100644 index 00000000000..9b31bfbbe5f --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/__init__.py @@ -0,0 +1,33 @@ +"""Command models for Flex Stacker commands.""" + +from .store import ( + StoreCommandType, + StoreParams, + StoreResult, + Store, + StoreCreate, +) + +from .retrieve import ( + RetrieveCommandType, + RetrieveParams, + RetrieveResult, + Retrieve, + RetrieveCreate, +) + + +__all__ = [ + # flexStacker/store + "StoreCommandType", + "StoreParams", + "StoreResult", + "Store", + "StoreCreate", + # flexStacker/retrieve + "RetrieveCommandType", + "RetrieveParams", + "RetrieveResult", + "Retrieve", + "RetrieveCreate", +] diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py new file mode 100644 index 00000000000..e561e628fb0 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py @@ -0,0 +1,77 @@ +"""Command models to retrieve a labware from a Flex Stacker.""" +from __future__ import annotations +from typing import Optional, Literal, TYPE_CHECKING +from typing_extensions import Type + +from pydantic import BaseModel, Field + +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence +from ...state import update_types + +if TYPE_CHECKING: + from opentrons.protocol_engine.state.state import StateView + from opentrons.protocol_engine.execution import EquipmentHandler + +RetrieveCommandType = Literal["flexStacker/retrieve"] + + +class RetrieveParams(BaseModel): + """Input parameters for a labware retrieval command.""" + + moduleId: str = Field( + ..., + description="Unique ID of the Flex Stacker.", + ) + + +class RetrieveResult(BaseModel): + """Result data from a labware retrieval command.""" + + +class RetrieveImpl(AbstractCommandImpl[RetrieveParams, SuccessData[RetrieveResult]]): + """Implementation of a labware retrieval command.""" + + def __init__( + self, + state_view: StateView, + equipment: EquipmentHandler, + **kwargs: object, + ) -> None: + self._state_view = state_view + self._equipment = equipment + + async def execute(self, params: RetrieveParams) -> SuccessData[RetrieveResult]: + """Execute the labware retrieval command.""" + state_update = update_types.StateUpdate() + stacker_substate = self._state_view.modules.get_flex_stacker_substate( + module_id=params.moduleId + ) + + # Allow propagation of ModuleNotAttachedError. + stacker = self._equipment.get_module_hardware_api(stacker_substate.module_id) + + if stacker is not None: + # TODO: get labware height from labware state view + await stacker.dispense_labware(labware_height=50.0) + + return SuccessData(public=RetrieveResult(), state_update=state_update) + + +class Retrieve(BaseCommand[RetrieveParams, RetrieveResult, ErrorOccurrence]): + """A command to retrieve a labware from a Flex Stacker.""" + + commandType: RetrieveCommandType = "flexStacker/retrieve" + params: RetrieveParams + result: Optional[RetrieveResult] + + _ImplementationCls: Type[RetrieveImpl] = RetrieveImpl + + +class RetrieveCreate(BaseCommandCreate[RetrieveParams]): + """A request to execute a Flex Stacker retrieve command.""" + + commandType: RetrieveCommandType = "flexStacker/retrieve" + params: RetrieveParams + + _CommandCls: Type[Retrieve] = Retrieve diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py new file mode 100644 index 00000000000..918105d9c68 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/store.py @@ -0,0 +1,78 @@ +"""Command models to retrieve a labware from a Flex Stacker.""" +from __future__ import annotations +from typing import Optional, Literal, TYPE_CHECKING +from typing_extensions import Type + +from pydantic import BaseModel, Field + +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence +from ...state import update_types + +if TYPE_CHECKING: + from opentrons.protocol_engine.state.state import StateView + from opentrons.protocol_engine.execution import EquipmentHandler + + +StoreCommandType = Literal["flexStacker/store"] + + +class StoreParams(BaseModel): + """Input parameters for a labware storage command.""" + + moduleId: str = Field( + ..., + description="Unique ID of the flex stacker.", + ) + + +class StoreResult(BaseModel): + """Result data from a labware storage command.""" + + +class StoreImpl(AbstractCommandImpl[StoreParams, SuccessData[StoreResult]]): + """Implementation of a labware storage command.""" + + def __init__( + self, + state_view: StateView, + equipment: EquipmentHandler, + **kwargs: object, + ) -> None: + self._state_view = state_view + self._equipment = equipment + + async def execute(self, params: StoreParams) -> SuccessData[StoreResult]: + """Execute the labware storage command.""" + state_update = update_types.StateUpdate() + stacker_substate = self._state_view.modules.get_flex_stacker_substate( + module_id=params.moduleId + ) + + # Allow propagation of ModuleNotAttachedError. + stacker = self._equipment.get_module_hardware_api(stacker_substate.module_id) + + if stacker is not None: + # TODO: get labware height from labware state view + await stacker.store_labware(labware_height=50.0) + + return SuccessData(public=StoreResult(), state_update=state_update) + + +class Store(BaseCommand[StoreParams, StoreResult, ErrorOccurrence]): + """A command to store a labware in a Flex Stacker.""" + + commandType: StoreCommandType = "flexStacker/store" + params: StoreParams + result: Optional[StoreResult] + + _ImplementationCls: Type[StoreImpl] = StoreImpl + + +class StoreCreate(BaseCommandCreate[StoreParams]): + """A request to execute a Flex Stacker store command.""" + + commandType: StoreCommandType = "flexStacker/store" + params: StoreParams + + _CommandCls: Type[Store] = Store diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 6b65fe239e4..d0e83863616 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -172,6 +172,19 @@ async def execute( top_labware_definition=loaded_labware.definition, bottom_labware_id=verified_location.labwareId, ) + # Validate load location is valid for lids + if ( + labware_validation.validate_definition_is_lid( + definition=loaded_labware.definition + ) + and loaded_labware.definition.compatibleParentLabware is not None + and self._state_view.labware.get_load_name(verified_location.labwareId) + not in loaded_labware.definition.compatibleParentLabware + ): + raise ValueError( + f"Labware Lid {params.loadName} may not be loaded on parent labware {self._state_view.labware.get_display_name(verified_location.labwareId)}." + ) + # Validate labware for the absorbance reader elif isinstance(params.location, ModuleLocation): module = self._state_view.modules.get(params.location.moduleId) @@ -179,7 +192,6 @@ async def execute( self._state_view.labware.raise_if_labware_incompatible_with_plate_reader( loaded_labware.definition ) - return SuccessData( public=LoadLabwareResult( labwareId=loaded_labware.labware_id, diff --git a/api/src/opentrons/protocol_engine/commands/load_lid.py b/api/src/opentrons/protocol_engine/commands/load_lid.py new file mode 100644 index 00000000000..4f2e49c7447 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/load_lid.py @@ -0,0 +1,146 @@ +"""Load lid command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal + +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + +from ..errors import LabwareCannotBeStackedError, LabwareIsNotAllowedInLocationError +from ..resources import labware_validation +from ..types import ( + LabwareLocation, + OnLabwareLocation, +) + +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence +from ..state.update_types import StateUpdate + +if TYPE_CHECKING: + from ..state.state import StateView + from ..execution import EquipmentHandler + + +LoadLidCommandType = Literal["loadLid"] + + +class LoadLidParams(BaseModel): + """Payload required to load a lid onto a labware.""" + + location: LabwareLocation = Field( + ..., + description="Labware the lid should be loaded onto.", + ) + loadName: str = Field( + ..., + description="Name used to reference a lid labware definition.", + ) + namespace: str = Field( + ..., + description="The namespace the lid labware definition belongs to.", + ) + version: int = Field( + ..., + description="The lid labware definition version.", + ) + + +class LoadLidResult(BaseModel): + """Result data from the execution of a LoadLabware command.""" + + labwareId: str = Field( + ..., + description="An ID to reference this lid labware in subsequent commands.", + ) + definition: LabwareDefinition = Field( + ..., + description="The full definition data for this lid labware.", + ) + + +class LoadLidImplementation( + AbstractCommandImpl[LoadLidParams, SuccessData[LoadLidResult]] +): + """Load lid command implementation.""" + + def __init__( + self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object + ) -> None: + self._equipment = equipment + self._state_view = state_view + + async def execute(self, params: LoadLidParams) -> SuccessData[LoadLidResult]: + """Load definition and calibration data necessary for a lid.""" + if not isinstance(params.location, OnLabwareLocation): + raise LabwareIsNotAllowedInLocationError( + "Lid Labware is only allowed to be loaded on top of a labware. Try `load_lid_stack(...)` to load lids without parent labware." + ) + + verified_location = self._state_view.geometry.ensure_location_not_occupied( + params.location + ) + loaded_labware = await self._equipment.load_labware( + load_name=params.loadName, + namespace=params.namespace, + version=params.version, + location=verified_location, + labware_id=None, + ) + + # TODO(chb 2024-12-12) these validation checks happen after the labware is loaded, because they rely on + # on the definition. In practice this will not cause any issues since they will raise protocol ending + # exception, but for correctness should be refactored to do this check beforehand. + if not labware_validation.validate_definition_is_lid(loaded_labware.definition): + raise LabwareCannotBeStackedError( + f"Labware {params.loadName} is not a Lid and cannot be loaded onto {self._state_view.labware.get_display_name(params.location.labwareId)}." + ) + + state_update = StateUpdate() + + # In the case of lids being loaded on top of other labware, set the parent labware's lid + state_update.set_lid( + parent_labware_id=params.location.labwareId, + lid_id=loaded_labware.labware_id, + ) + + state_update.set_loaded_labware( + labware_id=loaded_labware.labware_id, + offset_id=loaded_labware.offsetId, + definition=loaded_labware.definition, + location=verified_location, + display_name=None, + ) + + if isinstance(verified_location, OnLabwareLocation): + self._state_view.labware.raise_if_labware_cannot_be_stacked( + top_labware_definition=loaded_labware.definition, + bottom_labware_id=verified_location.labwareId, + ) + + return SuccessData( + public=LoadLidResult( + labwareId=loaded_labware.labware_id, + definition=loaded_labware.definition, + ), + state_update=state_update, + ) + + +class LoadLid(BaseCommand[LoadLidParams, LoadLidResult, ErrorOccurrence]): + """Load lid command resource model.""" + + commandType: LoadLidCommandType = "loadLid" + params: LoadLidParams + result: Optional[LoadLidResult] + + _ImplementationCls: Type[LoadLidImplementation] = LoadLidImplementation + + +class LoadLidCreate(BaseCommandCreate[LoadLidParams]): + """Load lid command creation request.""" + + commandType: LoadLidCommandType = "loadLid" + params: LoadLidParams + + _CommandCls: Type[LoadLid] = LoadLid diff --git a/api/src/opentrons/protocol_engine/commands/load_lid_stack.py b/api/src/opentrons/protocol_engine/commands/load_lid_stack.py new file mode 100644 index 00000000000..7b430dfaf45 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/load_lid_stack.py @@ -0,0 +1,189 @@ +"""Load lid stack command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, Type, List +from typing_extensions import Literal + +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + +from ..errors import LabwareIsNotAllowedInLocationError, ProtocolEngineError +from ..resources import fixture_validation, labware_validation +from ..types import ( + LabwareLocation, + OnLabwareLocation, + DeckSlotLocation, + AddressableAreaLocation, +) + +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence +from ..state.update_types import StateUpdate + +if TYPE_CHECKING: + from ..state.state import StateView + from ..execution import EquipmentHandler + + +LoadLidStackCommandType = Literal["loadLidStack"] + +_LID_STACK_PE_LABWARE = "protocol_engine_lid_stack_object" +_LID_STACK_PE_NAMESPACE = "opentrons" +_LID_STACK_PE_VERSION = 1 + + +class LoadLidStackParams(BaseModel): + """Payload required to load a lid stack onto a location.""" + + location: LabwareLocation = Field( + ..., + description="Location the lid stack should be loaded into.", + ) + loadName: str = Field( + ..., + description="Name used to reference a lid labware definition.", + ) + namespace: str = Field( + ..., + description="The namespace the lid labware definition belongs to.", + ) + version: int = Field( + ..., + description="The lid labware definition version.", + ) + quantity: int = Field( + ..., + description="The quantity of lids to load.", + ) + + +class LoadLidStackResult(BaseModel): + """Result data from the execution of a LoadLidStack command.""" + + stackLabwareId: str = Field( + ..., + description="An ID to reference the Protocol Engine Labware Lid Stack in subsequent commands.", + ) + labwareIds: List[str] = Field( + ..., + description="A list of lid labware IDs to reference the lids in this stack by. The first ID is the bottom of the stack.", + ) + definition: LabwareDefinition = Field( + ..., + description="The full definition data for this lid labware.", + ) + location: LabwareLocation = Field( + ..., description="The Location that the stack of lid labware has been loaded." + ) + + +class LoadLidStackImplementation( + AbstractCommandImpl[LoadLidStackParams, SuccessData[LoadLidStackResult]] +): + """Load lid stack command implementation.""" + + def __init__( + self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object + ) -> None: + self._equipment = equipment + self._state_view = state_view + + async def execute( + self, params: LoadLidStackParams + ) -> SuccessData[LoadLidStackResult]: + """Load definition and calibration data necessary for a lid stack.""" + if isinstance(params.location, AddressableAreaLocation): + area_name = params.location.addressableAreaName + if not ( + fixture_validation.is_deck_slot(params.location.addressableAreaName) + or fixture_validation.is_abs_reader(params.location.addressableAreaName) + ): + raise LabwareIsNotAllowedInLocationError( + f"Cannot load {params.loadName} onto addressable area {area_name}" + ) + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + area_name + ) + elif isinstance(params.location, DeckSlotLocation): + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.location.slotName.id + ) + + verified_location = self._state_view.geometry.ensure_location_not_occupied( + params.location + ) + + lid_stack_object = await self._equipment.load_labware( + load_name=_LID_STACK_PE_LABWARE, + namespace=_LID_STACK_PE_NAMESPACE, + version=_LID_STACK_PE_VERSION, + location=verified_location, + labware_id=None, + ) + if not labware_validation.validate_definition_is_system( + lid_stack_object.definition + ): + raise ProtocolEngineError( + message="Lid Stack Labware Object Labware Definition does not contain required allowed role 'system'." + ) + + loaded_lid_labwares = await self._equipment.load_lids( + load_name=params.loadName, + namespace=params.namespace, + version=params.version, + location=OnLabwareLocation(labwareId=lid_stack_object.labware_id), + quantity=params.quantity, + ) + loaded_lid_locations_by_id = {} + load_location = OnLabwareLocation(labwareId=lid_stack_object.labware_id) + for loaded_lid in loaded_lid_labwares: + loaded_lid_locations_by_id[loaded_lid.labware_id] = load_location + load_location = OnLabwareLocation(labwareId=loaded_lid.labware_id) + + state_update = StateUpdate() + state_update.set_loaded_lid_stack( + stack_id=lid_stack_object.labware_id, + stack_object_definition=lid_stack_object.definition, + stack_location=verified_location, + labware_ids=list(loaded_lid_locations_by_id.keys()), + labware_definition=loaded_lid_labwares[0].definition, + locations=loaded_lid_locations_by_id, + ) + + if isinstance(verified_location, OnLabwareLocation): + self._state_view.labware.raise_if_labware_cannot_be_stacked( + top_labware_definition=loaded_lid_labwares[ + params.quantity - 1 + ].definition, + bottom_labware_id=verified_location.labwareId, + ) + + return SuccessData( + public=LoadLidStackResult( + stackLabwareId=lid_stack_object.labware_id, + labwareIds=list(loaded_lid_locations_by_id.keys()), + definition=loaded_lid_labwares[0].definition, + location=params.location, + ), + state_update=state_update, + ) + + +class LoadLidStack( + BaseCommand[LoadLidStackParams, LoadLidStackResult, ErrorOccurrence] +): + """Load lid stack command resource model.""" + + commandType: LoadLidStackCommandType = "loadLidStack" + params: LoadLidStackParams + result: Optional[LoadLidStackResult] + + _ImplementationCls: Type[LoadLidStackImplementation] = LoadLidStackImplementation + + +class LoadLidStackCreate(BaseCommandCreate[LoadLidStackParams]): + """Load lid stack command creation request.""" + + commandType: LoadLidStackCommandType = "loadLidStack" + params: LoadLidStackParams + + _CommandCls: Type[LoadLidStack] = LoadLidStack diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 792bd583b88..8b23c3a0173 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -1,11 +1,11 @@ """Equipment command side-effect logic.""" from dataclasses import dataclass -from typing import Optional, overload, Union +from typing import Optional, overload, Union, List +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.calibration_storage.helpers import uri_from_details -from opentrons.protocols.models import LabwareDefinition from opentrons.types import MountType from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.modules import ( @@ -15,6 +15,7 @@ TempDeck, Thermocycler, AbsorbanceReader, + FlexStacker, ) from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_engine.state.module_substates import ( @@ -23,6 +24,7 @@ TemperatureModuleId, ThermocyclerModuleId, AbsorbanceReaderId, + FlexStackerId, ) from ..errors import ( FailedToLoadPipetteError, @@ -152,10 +154,6 @@ async def load_labware( Returns: A LoadedLabwareData object. """ - labware_id = ( - labware_id if labware_id is not None else self._model_utils.generate_id() - ) - definition_uri = uri_from_details( load_name=load_name, namespace=namespace, @@ -172,6 +170,10 @@ async def load_labware( version=version, ) + labware_id = ( + labware_id if labware_id is not None else self._model_utils.generate_id() + ) + # Allow propagation of ModuleNotLoadedError. offset_id = self.find_applicable_labware_offset_id( labware_definition_uri=definition_uri, @@ -379,6 +381,74 @@ async def load_module( definition=attached_module.definition, ) + async def load_lids( + self, + load_name: str, + namespace: str, + version: int, + location: LabwareLocation, + quantity: int, + ) -> List[LoadedLabwareData]: + """Load one or many lid labware by assigning an identifier and pulling required data. + + Args: + load_name: The lid labware's load name. + namespace: The lid labware's namespace. + version: The lid labware's version. + location: The deck location at which lid(s) will be placed. + labware_ids: An optional list of identifiers to assign the labware. If None, + an identifier will be generated. + + Raises: + ModuleNotLoadedError: If `location` references a module ID + that doesn't point to a valid loaded module. + + Returns: + A list of LoadedLabwareData objects. + """ + definition_uri = uri_from_details( + load_name=load_name, + namespace=namespace, + version=version, + ) + try: + # Try to use existing definition in state. + definition = self._state_store.labware.get_definition_by_uri(definition_uri) + except LabwareDefinitionDoesNotExistError: + definition = await self._labware_data_provider.get_labware_definition( + load_name=load_name, + namespace=namespace, + version=version, + ) + + stack_limit = definition.stackLimit if definition.stackLimit is not None else 1 + if quantity > stack_limit: + raise ValueError( + f"Requested quantity {quantity} is greater than the stack limit of {stack_limit} provided by definition for {load_name}." + ) + + # Allow propagation of ModuleNotLoadedError. + if ( + isinstance(location, DeckSlotLocation) + and definition.parameters.isDeckSlotCompatible is not None + and not definition.parameters.isDeckSlotCompatible + ): + raise ValueError( + f"Lid Labware {load_name} cannot be loaded onto a Deck Slot." + ) + + load_labware_data_list = [] + for i in range(quantity): + load_labware_data_list.append( + LoadedLabwareData( + labware_id=self._model_utils.generate_id(), + definition=definition, + offsetId=None, + ) + ) + + return load_labware_data_list + async def configure_for_volume( self, pipette_id: str, volume: float, tip_overlap_version: Optional[str] ) -> LoadedConfigureForVolumeData: @@ -513,6 +583,13 @@ def get_module_hardware_api( ) -> Optional[AbsorbanceReader]: ... + @overload + def get_module_hardware_api( + self, + module_id: FlexStackerId, + ) -> Optional[FlexStacker]: + ... + def get_module_hardware_api(self, module_id: str) -> Optional[AbstractModule]: """Get the hardware API for a given module.""" use_virtual_modules = self._state_store.config.use_virtual_modules diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 92d992016cd..d1636d18001 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -2,20 +2,21 @@ from contextlib import AsyncExitStack from logging import getLogger from typing import Dict, Optional, Union, AsyncGenerator, Callable -from opentrons.protocol_engine.actions.actions import ( - ResumeFromRecoveryAction, - SetErrorRecoveryPolicyAction, -) -from opentrons.protocols.models import LabwareDefinition -from opentrons.hardware_control import HardwareControlAPI -from opentrons.hardware_control.modules import AbstractModule as HardwareModuleAPI -from opentrons.hardware_control.types import PauseType as HardwarePauseType from opentrons_shared_data.errors import ( ErrorCodes, EnumeratedError, ) +from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control.modules import AbstractModule as HardwareModuleAPI +from opentrons.hardware_control.types import PauseType as HardwarePauseType + +from .actions.actions import ( + ResumeFromRecoveryAction, + SetErrorRecoveryPolicyAction, +) from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllowedError from .errors.exceptions import EStopActivatedError from .error_recovery_policy import ErrorRecoveryPolicy diff --git a/api/src/opentrons/protocol_engine/resources/deck_data_provider.py b/api/src/opentrons/protocol_engine/resources/deck_data_provider.py index 72117c23075..5249508a3dd 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_data_provider.py @@ -10,7 +10,7 @@ DEFAULT_DECK_DEFINITION_VERSION, ) from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons.types import DeckSlotName from ..types import ( diff --git a/api/src/opentrons/protocol_engine/resources/labware_data_provider.py b/api/src/opentrons/protocol_engine/resources/labware_data_provider.py index 8d5cdfc7899..b71f7e6dac1 100644 --- a/api/src/opentrons/protocol_engine/resources/labware_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/labware_data_provider.py @@ -6,7 +6,8 @@ import logging from anyio import to_thread -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from opentrons.protocols.labware import get_labware_definition # TODO (lc 09-26-2022) We should conditionally import ot2 or ot3 calibration diff --git a/api/src/opentrons/protocol_engine/resources/labware_validation.py b/api/src/opentrons/protocol_engine/resources/labware_validation.py index 090723ffb7e..924d0fc0a24 100644 --- a/api/src/opentrons/protocol_engine/resources/labware_validation.py +++ b/api/src/opentrons/protocol_engine/resources/labware_validation.py @@ -1,7 +1,9 @@ """Validation file for labware role and location checking functions.""" -from opentrons_shared_data.labware.labware_definition import LabwareRole -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + LabwareRole, +) def is_flex_trash(load_name: str) -> bool: @@ -32,6 +34,11 @@ def validate_definition_is_lid(definition: LabwareDefinition) -> bool: return LabwareRole.lid in definition.allowedRoles +def validate_definition_is_system(definition: LabwareDefinition) -> bool: + """Validate that one of the definition's allowed roles is `system`.""" + return LabwareRole.system in definition.allowedRoles + + def validate_labware_can_be_stacked( top_labware_definition: LabwareDefinition, below_labware_load_name: str ) -> bool: diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 83499fb2510..b28fb936be7 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -82,19 +82,12 @@ def _circular_frustum_polynomial_roots( def _volume_from_height_circular( - target_height: float, - total_frustum_height: float, - bottom_radius: float, - top_radius: float, + target_height: float, segment: ConicalFrustum ) -> float: """Find the volume given a height within a circular frustum.""" - a, b, c = _circular_frustum_polynomial_roots( - bottom_radius=bottom_radius, - top_radius=top_radius, - total_frustum_height=total_frustum_height, - ) - volume = a * (target_height**3) + b * (target_height**2) + c * target_height - return volume + heights = segment.height_to_volume_table.keys() + best_fit_height = min(heights, key=lambda x: abs(x - target_height)) + return segment.height_to_volume_table[best_fit_height] def _volume_from_height_rectangular( @@ -138,26 +131,12 @@ def _volume_from_height_squared_cone( def _height_from_volume_circular( - volume: float, - total_frustum_height: float, - bottom_radius: float, - top_radius: float, + target_volume: float, segment: ConicalFrustum ) -> float: - """Find the height given a volume within a circular frustum.""" - a, b, c = _circular_frustum_polynomial_roots( - bottom_radius=bottom_radius, - top_radius=top_radius, - total_frustum_height=total_frustum_height, - ) - d = volume * -1 - x_intercept_roots = (a, b, c, d) - - height_from_volume_roots = roots(x_intercept_roots) - height = _reject_unacceptable_heights( - potential_heights=list(height_from_volume_roots), - max_height=total_frustum_height, - ) - return height + """Find the height given a volume within a squared cone segment.""" + volumes = segment.volume_to_height_table.keys() + best_fit_volume = min(volumes, key=lambda x: abs(x - target_volume)) + return segment.volume_to_height_table[best_fit_volume] def _height_from_volume_rectangular( @@ -243,9 +222,7 @@ def _get_segment_capacity(segment: WellSegment) -> float: return ( _volume_from_height_circular( target_height=section_height, - total_frustum_height=section_height, - bottom_radius=(segment.bottomDiameter / 2), - top_radius=(segment.topDiameter / 2), + segment=segment, ) * segment.count ) @@ -293,12 +270,7 @@ def height_at_volume_within_section( radius_of_curvature=section.radiusOfCurvature, ) case ConicalFrustum(): - return _height_from_volume_circular( - volume=target_volume_relative, - top_radius=(section.bottomDiameter / 2), - bottom_radius=(section.topDiameter / 2), - total_frustum_height=section_height, - ) + return _height_from_volume_circular(target_volume_relative, section) case CuboidalFrustum(): return _height_from_volume_rectangular( volume=target_volume_relative, @@ -334,10 +306,7 @@ def volume_at_height_within_section( case ConicalFrustum(): return ( _volume_from_height_circular( - target_height=target_height_relative, - total_frustum_height=section_height, - bottom_radius=(section.bottomDiameter / 2), - top_radius=(section.topDiameter / 2), + target_height=target_height_relative, segment=section ) * section.count ) @@ -427,7 +396,7 @@ def _find_height_in_partial_frustum( if ( bottom_section_volume < target_volume - < (bottom_section_volume + section_volume) + <= (bottom_section_volume + section_volume) ): relative_target_volume = target_volume - bottom_section_volume section_height = section.topHeight - section.bottomHeight diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index e0d9cb1afa1..7c725d88c62 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -11,10 +11,10 @@ from opentrons_shared_data.errors.exceptions import InvalidStoredData from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.deck.types import CutoutFixture from opentrons_shared_data.pipette import PIPETTE_X_SPAN from opentrons_shared_data.pipette.types import ChannelCount -from opentrons.protocols.models import LabwareDefinition from .. import errors from ..errors import ( diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 3f00ad14de7..34e69a57a3b 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -20,14 +20,15 @@ from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE from opentrons_shared_data.labware.labware_definition import ( - LabwareRole, InnerWellGeometry, + LabwareDefinition, + LabwareRole, + WellDefinition, ) from opentrons_shared_data.pipette.types import LabwareUri from opentrons.types import DeckSlotName, StagingSlotName, MountType from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE -from opentrons.protocols.models import LabwareDefinition, WellDefinition from opentrons.calibration_storage.helpers import uri_from_details from .. import errors @@ -156,7 +157,9 @@ def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" for state_update in get_state_updates(action): self._add_loaded_labware(state_update) + self._add_loaded_lid_stack(state_update) self._set_labware_location(state_update) + self._set_labware_lid(state_update) if isinstance(action, AddLabwareOffsetAction): labware_offset = LabwareOffset.model_construct( @@ -221,6 +224,63 @@ def _add_loaded_labware(self, state_update: update_types.StateUpdate) -> None: displayName=display_name, ) + def _add_loaded_lid_stack(self, state_update: update_types.StateUpdate) -> None: + loaded_lid_stack_update = state_update.loaded_lid_stack + if loaded_lid_stack_update != update_types.NO_CHANGE: + # Add the stack object + stack_definition_uri = uri_from_details( + namespace=loaded_lid_stack_update.stack_object_definition.namespace, + load_name=loaded_lid_stack_update.stack_object_definition.parameters.loadName, + version=loaded_lid_stack_update.stack_object_definition.version, + ) + self.state.definitions_by_uri[ + stack_definition_uri + ] = loaded_lid_stack_update.stack_object_definition + self._state.labware_by_id[ + loaded_lid_stack_update.stack_id + ] = LoadedLabware.construct( + id=loaded_lid_stack_update.stack_id, + location=loaded_lid_stack_update.stack_location, + loadName=loaded_lid_stack_update.stack_object_definition.parameters.loadName, + definitionUri=stack_definition_uri, + offsetId=None, + displayName=None, + ) + + # Add the Lids on top of the stack object + for i in range(len(loaded_lid_stack_update.labware_ids)): + definition_uri = uri_from_details( + namespace=loaded_lid_stack_update.definition.namespace, + load_name=loaded_lid_stack_update.definition.parameters.loadName, + version=loaded_lid_stack_update.definition.version, + ) + + self._state.definitions_by_uri[ + definition_uri + ] = loaded_lid_stack_update.definition + + location = loaded_lid_stack_update.new_locations_by_id[ + loaded_lid_stack_update.labware_ids[i] + ] + + self._state.labware_by_id[ + loaded_lid_stack_update.labware_ids[i] + ] = LoadedLabware.construct( + id=loaded_lid_stack_update.labware_ids[i], + location=location, + loadName=loaded_lid_stack_update.definition.parameters.loadName, + definitionUri=definition_uri, + offsetId=None, + displayName=None, + ) + + def _set_labware_lid(self, state_update: update_types.StateUpdate) -> None: + labware_lid_update = state_update.labware_lid + if labware_lid_update != update_types.NO_CHANGE: + parent_labware_id = labware_lid_update.parent_labware_id + lid_id = labware_lid_update.lid_id + self._state.labware_by_id[parent_labware_id].lid_id = lid_id + def _set_labware_location(self, state_update: update_types.StateUpdate) -> None: labware_location_update = state_update.labware_location if labware_location_update != update_types.NO_CHANGE: @@ -441,21 +501,7 @@ def get_labware_stacking_maximum(self, labware: LabwareDefinition) -> int: If not defined within a labware, defaults to one. """ - stacking_quirks = { - "stackingMaxFive": 5, - "stackingMaxFour": 4, - "stackingMaxThree": 3, - "stackingMaxTwo": 2, - "stackingMaxOne": 1, - "stackingMaxZero": 0, - } - for quirk in stacking_quirks.keys(): - if ( - labware.parameters.quirks is not None - and quirk in labware.parameters.quirks - ): - return stacking_quirks[quirk] - return 1 + return labware.stackLimit if labware.stackLimit is not None else 1 def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool: """True if a pipette moving to a well of this labware should center its body on the target. @@ -479,7 +525,6 @@ def get_well_definition( will be used. """ definition = self.get_definition(labware_id) - if well_name is None: well_name = definition.ordering[0][0] @@ -815,6 +860,11 @@ def raise_if_labware_inaccessible_by_pipette(self, labware_id: str) -> None: return self.raise_if_labware_inaccessible_by_pipette( labware_location.labwareId ) + elif labware.lid_id is not None: + raise errors.LocationNotAccessibleByPipetteError( + f"Cannot move pipette to {labware.loadName} " + "because labware is currently covered by a lid." + ) elif isinstance(labware_location, AddressableAreaLocation): if fixture_validation.is_staging_slot(labware_location.addressableAreaName): raise errors.LocationNotAccessibleByPipetteError( diff --git a/api/src/opentrons/protocol_engine/state/module_substates/__init__.py b/api/src/opentrons/protocol_engine/state/module_substates/__init__.py index 3865982612b..a2f83036fbd 100644 --- a/api/src/opentrons/protocol_engine/state/module_substates/__init__.py +++ b/api/src/opentrons/protocol_engine/state/module_substates/__init__.py @@ -13,6 +13,7 @@ ) from .magnetic_block_substate import MagneticBlockSubState, MagneticBlockId from .absorbance_reader_substate import AbsorbanceReaderSubState, AbsorbanceReaderId +from .flex_stacker_substate import FlexStackerSubState, FlexStackerId ModuleSubStateType = Union[ HeaterShakerModuleSubState, @@ -21,6 +22,7 @@ ThermocyclerModuleSubState, MagneticBlockSubState, AbsorbanceReaderSubState, + FlexStackerSubState, ] __all__ = [ @@ -36,6 +38,8 @@ "MagneticBlockId", "AbsorbanceReaderSubState", "AbsorbanceReaderId", + "FlexStackerSubState", + "FlexStackerId", # Union of all module substates "ModuleSubStateType", ] diff --git a/api/src/opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py b/api/src/opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py new file mode 100644 index 00000000000..67690a0750a --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py @@ -0,0 +1,17 @@ +"""Flex Stacker substate.""" +from dataclasses import dataclass +from typing import NewType + + +FlexStackerId = NewType("FlexStackerId", str) + + +@dataclass(frozen=True) +class FlexStackerSubState: + """Flex Stacker-specific state. + + Provides calculations and read-only state access + for an individual loaded Flex Stacker Module. + """ + + module_id: FlexStackerId diff --git a/api/src/opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py b/api/src/opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py index 056c6b62db7..1ed0fc06b60 100644 --- a/api/src/opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +++ b/api/src/opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import NewType, Optional +from opentrons.hardware_control.modules import ModuleDataValidator, ModuleData from opentrons.protocol_engine.types import ( TemperatureRange, SpeedRange, @@ -89,3 +90,24 @@ def raise_if_shaking(self) -> None: raise CannotPerformModuleAction( "Heater-Shaker cannot open its labware latch while it is shaking." ) + + @classmethod + def from_live_data( + cls, module_id: HeaterShakerModuleId, data: ModuleData | None + ) -> "HeaterShakerModuleSubState": + """Create a HeaterShakerModuleSubState from live data.""" + if ModuleDataValidator.is_heater_shaker_data(data): + return cls( + module_id=module_id, + labware_latch_status=HeaterShakerLatchStatus.CLOSED + if data["labwareLatchStatus"] == "idle_closed" + else HeaterShakerLatchStatus.OPEN, + is_plate_shaking=data["targetSpeed"] is not None, + plate_target_temperature=data["targetTemp"], + ) + return cls( + module_id=module_id, + labware_latch_status=HeaterShakerLatchStatus.UNKNOWN, + is_plate_shaking=False, + plate_target_temperature=None, + ) diff --git a/api/src/opentrons/protocol_engine/state/module_substates/temperature_module_substate.py b/api/src/opentrons/protocol_engine/state/module_substates/temperature_module_substate.py index 64f13e47744..a3a33c38802 100644 --- a/api/src/opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +++ b/api/src/opentrons/protocol_engine/state/module_substates/temperature_module_substate.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import NewType, Optional +from opentrons.hardware_control.modules import ModuleDataValidator, ModuleData from opentrons.protocol_engine.types import TemperatureRange from opentrons.protocol_engine.errors import ( InvalidTargetTemperatureError, @@ -52,3 +53,15 @@ def get_plate_target_temperature(self) -> float: f"Module {self.module_id} does not have a target temperature set." ) return target + + @classmethod + def from_live_data( + cls, module_id: TemperatureModuleId, data: ModuleData | None + ) -> "TemperatureModuleSubState": + """Create a TemperatureModuleSubState from live data.""" + return cls( + module_id=module_id, + plate_target_temperature=data["targetTemp"] + if ModuleDataValidator.is_temperature_module_data(data) + else None, + ) diff --git a/api/src/opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py b/api/src/opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py index bbf1bb901ca..7969f2ba696 100644 --- a/api/src/opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +++ b/api/src/opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py @@ -19,6 +19,7 @@ LID_TARGET_MIN, LID_TARGET_MAX, ) +from opentrons.hardware_control.modules import ModuleData, ModuleDataValidator ThermocyclerModuleId = NewType("ThermocyclerModuleId", str) @@ -141,3 +142,22 @@ def get_target_lid_temperature(self) -> float: f"Module {self.module_id} does not have a target block temperature set." ) return target + + @classmethod + def from_live_data( + cls, module_id: ThermocyclerModuleId, data: ModuleData | None + ) -> "ThermocyclerModuleSubState": + """Create a ThermocyclerModuleSubState from live data.""" + if ModuleDataValidator.is_thermocycler_data(data): + return cls( + module_id=module_id, + is_lid_open=data["lid"] == "open", + target_block_temperature=data["targetTemp"], + target_lid_temperature=data["lidTarget"], + ) + return cls( + module_id=module_id, + is_lid_open=False, + target_block_temperature=None, + target_lid_temperature=None, + ) diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index a0b22f14fcb..046f57ffc94 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -35,6 +35,7 @@ AbsorbanceReaderMeasureMode, ) from opentrons.types import DeckSlotName, MountType, StagingSlotName +from .update_types import AbsorbanceReaderStateUpdate, FlexStackerStateUpdate from ..errors import ModuleNotConnectedError from ..types import ( @@ -63,7 +64,6 @@ heater_shaker, temperature_module, thermocycler, - absorbance_reader, ) from ..actions import ( Action, @@ -77,11 +77,13 @@ TemperatureModuleSubState, ThermocyclerModuleSubState, AbsorbanceReaderSubState, + FlexStackerSubState, MagneticModuleId, HeaterShakerModuleId, TemperatureModuleId, ThermocyclerModuleId, AbsorbanceReaderId, + FlexStackerId, MagneticBlockSubState, MagneticBlockId, ModuleSubStateType, @@ -296,41 +298,13 @@ def _handle_command(self, command: Command) -> None: ): self._handle_thermocycler_module_commands(command) - if isinstance( - command.result, - ( - absorbance_reader.InitializeResult, - absorbance_reader.ReadAbsorbanceResult, - ), - ): - self._handle_absorbance_reader_commands(command) - def _handle_state_update(self, state_update: update_types.StateUpdate) -> None: - if state_update.absorbance_reader_lid != update_types.NO_CHANGE: - module_id = state_update.absorbance_reader_lid.module_id - is_lid_on = state_update.absorbance_reader_lid.is_lid_on - - # Get current values: - absorbance_reader_substate = self._state.substate_by_module_id[module_id] - assert isinstance( - absorbance_reader_substate, AbsorbanceReaderSubState - ), f"{module_id} is not an absorbance plate reader." - configured = absorbance_reader_substate.configured - measure_mode = absorbance_reader_substate.measure_mode - configured_wavelengths = absorbance_reader_substate.configured_wavelengths - reference_wavelength = absorbance_reader_substate.reference_wavelength - data = absorbance_reader_substate.data - - self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( - module_id=AbsorbanceReaderId(module_id), - configured=configured, - measured=True, - is_lid_on=is_lid_on, - measure_mode=measure_mode, - configured_wavelengths=configured_wavelengths, - reference_wavelength=reference_wavelength, - data=data, + if state_update.absorbance_reader_state_update != update_types.NO_CHANGE: + self._handle_absorbance_reader_commands( + state_update.absorbance_reader_state_update ) + if state_update.flex_stacker_state_update != update_types.NO_CHANGE: + self._handle_flex_stacker_commands(state_update.flex_stacker_state_update) def _add_module_substate( self, @@ -357,31 +331,24 @@ def _add_module_substate( model=actual_model, ) elif ModuleModel.is_heater_shaker_module_model(actual_model): - if live_data is None: - labware_latch_status = HeaterShakerLatchStatus.UNKNOWN - elif live_data["labwareLatchStatus"] == "idle_closed": - labware_latch_status = HeaterShakerLatchStatus.CLOSED - else: - labware_latch_status = HeaterShakerLatchStatus.OPEN - self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState( + self._state.substate_by_module_id[ + module_id + ] = HeaterShakerModuleSubState.from_live_data( module_id=HeaterShakerModuleId(module_id), - labware_latch_status=labware_latch_status, - is_plate_shaking=( - live_data is not None and live_data["targetSpeed"] is not None - ), - plate_target_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type] + data=live_data, ) elif ModuleModel.is_temperature_module_model(actual_model): - self._state.substate_by_module_id[module_id] = TemperatureModuleSubState( + self._state.substate_by_module_id[ + module_id + ] = TemperatureModuleSubState.from_live_data( module_id=TemperatureModuleId(module_id), - plate_target_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type] + data=live_data, ) elif ModuleModel.is_thermocycler_module_model(actual_model): - self._state.substate_by_module_id[module_id] = ThermocyclerModuleSubState( - module_id=ThermocyclerModuleId(module_id), - is_lid_open=live_data is not None and live_data["lid"] == "open", - target_block_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type] - target_lid_temperature=live_data["lidTarget"] if live_data else None, # type: ignore[arg-type] + self._state.substate_by_module_id[ + module_id + ] = ThermocyclerModuleSubState.from_live_data( + module_id=ThermocyclerModuleId(module_id), data=live_data ) self._update_additional_slots_occupied_by_thermocycler( module_id=module_id, slot_name=slot_name @@ -401,6 +368,10 @@ def _add_module_substate( configured_wavelengths=None, reference_wavelength=None, ) + elif ModuleModel.is_flex_stacker(actual_model): + self._state.substate_by_module_id[module_id] = FlexStackerSubState( + module_id=FlexStackerId(module_id), + ) def _update_additional_slots_occupied_by_thermocycler( self, @@ -589,47 +560,65 @@ def _handle_thermocycler_module_commands( ) def _handle_absorbance_reader_commands( - self, - command: Union[ - absorbance_reader.Initialize, - absorbance_reader.ReadAbsorbance, - ], + self, absorbance_reader_state_update: AbsorbanceReaderStateUpdate ) -> None: - module_id = command.params.moduleId + # Get current values: + module_id = absorbance_reader_state_update.module_id absorbance_reader_substate = self._state.substate_by_module_id[module_id] assert isinstance( absorbance_reader_substate, AbsorbanceReaderSubState ), f"{module_id} is not an absorbance plate reader." - - # Get current values + is_lid_on = absorbance_reader_substate.is_lid_on + measured = True configured = absorbance_reader_substate.configured measure_mode = absorbance_reader_substate.measure_mode configured_wavelengths = absorbance_reader_substate.configured_wavelengths reference_wavelength = absorbance_reader_substate.reference_wavelength - is_lid_on = absorbance_reader_substate.is_lid_on - - if isinstance(command.result, absorbance_reader.InitializeResult): - self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( - module_id=AbsorbanceReaderId(module_id), - configured=True, - measured=False, - is_lid_on=is_lid_on, - measure_mode=AbsorbanceReaderMeasureMode(command.params.measureMode), - configured_wavelengths=command.params.sampleWavelengths, - reference_wavelength=command.params.referenceWavelength, - data=None, + data = absorbance_reader_substate.data + if ( + absorbance_reader_state_update.absorbance_reader_lid + != update_types.NO_CHANGE + ): + is_lid_on = absorbance_reader_state_update.absorbance_reader_lid.is_lid_on + elif ( + absorbance_reader_state_update.initialize_absorbance_reader_update + != update_types.NO_CHANGE + ): + configured = True + measured = False + is_lid_on = is_lid_on + measure_mode = AbsorbanceReaderMeasureMode( + absorbance_reader_state_update.initialize_absorbance_reader_update.measure_mode ) - elif isinstance(command.result, absorbance_reader.ReadAbsorbanceResult): - self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( - module_id=AbsorbanceReaderId(module_id), - configured=configured, - measured=True, - is_lid_on=is_lid_on, - measure_mode=measure_mode, - configured_wavelengths=configured_wavelengths, - reference_wavelength=reference_wavelength, - data=command.result.data, + configured_wavelengths = ( + absorbance_reader_state_update.initialize_absorbance_reader_update.sample_wave_lengths + ) + reference_wavelength = ( + absorbance_reader_state_update.initialize_absorbance_reader_update.reference_wave_length ) + data = None + elif ( + absorbance_reader_state_update.absorbance_reader_data + != update_types.NO_CHANGE + ): + data = absorbance_reader_state_update.absorbance_reader_data.read_result + self._state.substate_by_module_id[module_id] = AbsorbanceReaderSubState( + module_id=AbsorbanceReaderId(module_id), + configured=configured, + measured=measured, + is_lid_on=is_lid_on, + measure_mode=measure_mode, + configured_wavelengths=configured_wavelengths, + reference_wavelength=reference_wavelength, + data=data, + ) + + def _handle_flex_stacker_commands( + self, flex_stacker_state_update: FlexStackerStateUpdate + ) -> None: + """Handle Flex Stacker state updates.""" + # TODO: Implement Flex Stacker state updates + pass class ModuleView: @@ -782,6 +771,20 @@ def get_absorbance_reader_substate( expected_name="Absorbance Reader", ) + def get_flex_stacker_substate(self, module_id: str) -> FlexStackerSubState: + """Return a `FlexStackerSubState` for the given Flex Stacker. + + Raises: + ModuleNotLoadedError: If module_id has not been loaded. + WrongModuleTypeError: If module_id has been loaded, + but it's not a Flex Stacker. + """ + return self._get_module_substate( + module_id=module_id, + expected_type=FlexStackerSubState, + expected_name="Flex Stacker", + ) + def get_location(self, module_id: str) -> DeckSlotLocation: """Get the slot location of the given module.""" location = self.get(module_id).location @@ -1315,6 +1318,10 @@ def ensure_and_convert_module_fixture_location( # only allowed in column 3 assert deck_slot.value[-1] == "3" return f"absorbanceReaderV1{deck_slot.value}" + elif model == ModuleModel.FLEX_STACKER_MODULE_V1: + # only allowed in column 4 + assert deck_slot.value[-1] == "4" + return f"flexStackerModuleV1{deck_slot.value}" raise ValueError( f"Unknown module {model.name} has no addressable areas to provide." diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py index 25b7802976c..a8075b2f883 100644 --- a/api/src/opentrons/protocol_engine/state/update_types.py +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -11,9 +11,11 @@ from opentrons.protocol_engine.types import ( DeckPoint, LabwareLocation, + OnLabwareLocation, TipGeometry, AspiratedFluid, LiquidClassRecord, + ABSMeasureMode, ) from opentrons.types import MountType from opentrons_shared_data.labware.labware_definition import LabwareDefinition @@ -120,6 +122,40 @@ class LoadedLabwareUpdate: definition: LabwareDefinition +@dataclasses.dataclass +class LoadedLidStackUpdate: + """An update that loads a new lid stack.""" + + stack_id: str + """The unique ID of the Lid Stack Object.""" + + stack_object_definition: LabwareDefinition + "The System-only Labware Definition of the Lid Stack Object" + + stack_location: LabwareLocation + "The initial location of the Lid Stack Object." + + labware_ids: typing.List[str] + """The unique IDs of the new lids.""" + + new_locations_by_id: typing.Dict[str, OnLabwareLocation] + """Each lid's initial location keyed by Labware ID.""" + + definition: LabwareDefinition + "The Labware Definition of the Lid Labware(s) loaded." + + +@dataclasses.dataclass +class LabwareLidUpdate: + """An update that identifies a lid on a given parent labware.""" + + parent_labware_id: str + """The unique ID of the parent labware.""" + + lid_id: str + """The unique IDs of the new lids.""" + + @dataclasses.dataclass class LoadPipetteUpdate: """An update that loads a new pipette. @@ -249,10 +285,44 @@ class PipetteEmptyFluidUpdate: class AbsorbanceReaderLidUpdate: """An update to an absorbance reader's lid location.""" - module_id: str is_lid_on: bool +@dataclasses.dataclass +class AbsorbanceReaderDataUpdate: + """An update to an absorbance reader's lid location.""" + + read_result: typing.Dict[int, typing.Dict[str, float]] + + +@dataclasses.dataclass(frozen=True) +class AbsorbanceReaderInitializeUpdate: + """An update to an absorbance reader's initialization.""" + + measure_mode: ABSMeasureMode + sample_wave_lengths: typing.List[int] + reference_wave_length: typing.Optional[int] + + +@dataclasses.dataclass +class AbsorbanceReaderStateUpdate: + """An update to the absorbance reader module state.""" + + module_id: str + absorbance_reader_lid: AbsorbanceReaderLidUpdate | NoChangeType = NO_CHANGE + absorbance_reader_data: AbsorbanceReaderDataUpdate | NoChangeType = NO_CHANGE + initialize_absorbance_reader_update: AbsorbanceReaderInitializeUpdate | NoChangeType = ( + NO_CHANGE + ) + + +@dataclasses.dataclass +class FlexStackerStateUpdate: + """An update to the Flex Stacker module state.""" + + module_id: str + + @dataclasses.dataclass class LiquidClassLoadedUpdate: """The state update from loading a liquid class.""" @@ -301,6 +371,10 @@ class StateUpdate: loaded_labware: LoadedLabwareUpdate | NoChangeType = NO_CHANGE + loaded_lid_stack: LoadedLidStackUpdate | NoChangeType = NO_CHANGE + + labware_lid: LabwareLidUpdate | NoChangeType = NO_CHANGE + tips_used: TipsUsedUpdate | NoChangeType = NO_CHANGE liquid_loaded: LiquidLoadedUpdate | NoChangeType = NO_CHANGE @@ -309,7 +383,11 @@ class StateUpdate: liquid_operated: LiquidOperatedUpdate | NoChangeType = NO_CHANGE - absorbance_reader_lid: AbsorbanceReaderLidUpdate | NoChangeType = NO_CHANGE + absorbance_reader_state_update: AbsorbanceReaderStateUpdate | NoChangeType = ( + NO_CHANGE + ) + + flex_stacker_state_update: FlexStackerStateUpdate | NoChangeType = NO_CHANGE liquid_class_loaded: LiquidClassLoadedUpdate | NoChangeType = NO_CHANGE @@ -442,6 +520,38 @@ def set_loaded_labware( ) return self + def set_loaded_lid_stack( + self: Self, + stack_id: str, + stack_object_definition: LabwareDefinition, + stack_location: LabwareLocation, + labware_definition: LabwareDefinition, + labware_ids: typing.List[str], + locations: typing.Dict[str, OnLabwareLocation], + ) -> Self: + """Add a new lid stack to state. See `LoadedLidStackUpdate`.""" + self.loaded_lid_stack = LoadedLidStackUpdate( + stack_id=stack_id, + stack_object_definition=stack_object_definition, + stack_location=stack_location, + definition=labware_definition, + labware_ids=labware_ids, + new_locations_by_id=locations, + ) + return self + + def set_lid( + self: Self, + parent_labware_id: str, + lid_id: str, + ) -> Self: + """Update the labware parent of a loaded or moved lid. See `LabwareLidUpdate`.""" + self.labware_lid = LabwareLidUpdate( + parent_labware_id=parent_labware_id, + lid_id=lid_id, + ) + return self + def set_load_pipette( self: Self, pipette_id: str, @@ -573,8 +683,40 @@ def set_fluid_empty(self: Self, pipette_id: str) -> Self: def set_absorbance_reader_lid(self: Self, module_id: str, is_lid_on: bool) -> Self: """Update an absorbance reader's lid location. See `AbsorbanceReaderLidUpdate`.""" - self.absorbance_reader_lid = AbsorbanceReaderLidUpdate( - module_id=module_id, is_lid_on=is_lid_on + assert self.absorbance_reader_state_update == NO_CHANGE + self.absorbance_reader_state_update = AbsorbanceReaderStateUpdate( + module_id=module_id, + absorbance_reader_lid=AbsorbanceReaderLidUpdate(is_lid_on=is_lid_on), + ) + return self + + def set_absorbance_reader_data( + self, module_id: str, read_result: typing.Dict[int, typing.Dict[str, float]] + ) -> Self: + """Update an absorbance reader's read data. See `AbsorbanceReaderReadDataUpdate`.""" + assert self.absorbance_reader_state_update == NO_CHANGE + self.absorbance_reader_state_update = AbsorbanceReaderStateUpdate( + module_id=module_id, + absorbance_reader_data=AbsorbanceReaderDataUpdate(read_result=read_result), + ) + return self + + def initialize_absorbance_reader( + self, + module_id: str, + measure_mode: ABSMeasureMode, + sample_wave_lengths: typing.List[int], + reference_wave_length: typing.Optional[int], + ) -> Self: + """Initialize absorbance reader.""" + assert self.absorbance_reader_state_update == NO_CHANGE + self.absorbance_reader_state_update = AbsorbanceReaderStateUpdate( + module_id=module_id, + initialize_absorbance_reader_update=AbsorbanceReaderInitializeUpdate( + measure_mode=measure_mode, + sample_wave_lengths=sample_wave_lengths, + reference_wave_length=reference_wave_length, + ), ) return self diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index b1388d58212..316c701eb20 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -476,6 +476,7 @@ class ModuleModel(str, Enum): HEATER_SHAKER_MODULE_V1 = "heaterShakerModuleV1" MAGNETIC_BLOCK_V1 = "magneticBlockV1" ABSORBANCE_READER_V1 = "absorbanceReaderV1" + FLEX_STACKER_MODULE_V1 = "flexStackerModuleV1" def as_type(self) -> ModuleType: """Get the ModuleType of this model.""" @@ -491,6 +492,8 @@ def as_type(self) -> ModuleType: return ModuleType.MAGNETIC_BLOCK elif ModuleModel.is_absorbance_reader(self): return ModuleType.ABSORBANCE_READER + elif ModuleModel.is_flex_stacker(self): + return ModuleType.FLEX_STACKER assert False, f"Invalid ModuleModel {self}" @@ -534,6 +537,11 @@ def is_absorbance_reader( """Whether a given model is an Absorbance Plate Reader.""" return model == cls.ABSORBANCE_READER_V1 + @classmethod + def is_flex_stacker(cls, model: ModuleModel) -> TypeGuard[FlexStackerModuleModel]: + """Whether a given model is a Flex Stacker..""" + return model == cls.FLEX_STACKER_MODULE_V1 + TemperatureModuleModel = Literal[ ModuleModel.TEMPERATURE_MODULE_V1, ModuleModel.TEMPERATURE_MODULE_V2 @@ -547,6 +555,7 @@ def is_absorbance_reader( HeaterShakerModuleModel = Literal[ModuleModel.HEATER_SHAKER_MODULE_V1] MagneticBlockModel = Literal[ModuleModel.MAGNETIC_BLOCK_V1] AbsorbanceReaderModel = Literal[ModuleModel.ABSORBANCE_READER_V1] +FlexStackerModuleModel = Literal[ModuleModel.FLEX_STACKER_MODULE_V1] class ModuleDimensions(BaseModel): @@ -804,6 +813,10 @@ class LoadedLabware(BaseModel): location: LabwareLocation = Field( ..., description="The labware's current location." ) + lid_id: Optional[str] = Field( + None, + description=("Labware ID of a Lid currently loaded on top of the labware."), + ) offsetId: Optional[str] = Field( None, description=( diff --git a/api/src/opentrons/protocol_reader/extract_labware_definitions.py b/api/src/opentrons/protocol_reader/extract_labware_definitions.py index 88d7e256a07..8ee4c820e87 100644 --- a/api/src/opentrons/protocol_reader/extract_labware_definitions.py +++ b/api/src/opentrons/protocol_reader/extract_labware_definitions.py @@ -6,7 +6,7 @@ import anyio -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from .protocol_source import ProtocolFileRole, ProtocolSource, ProtocolType diff --git a/api/src/opentrons/protocol_reader/file_format_validator.py b/api/src/opentrons/protocol_reader/file_format_validator.py index 17969fc70fe..ca664eced92 100644 --- a/api/src/opentrons/protocol_reader/file_format_validator.py +++ b/api/src/opentrons/protocol_reader/file_format_validator.py @@ -14,7 +14,7 @@ ) from opentrons_shared_data.errors.exceptions import PythonException -from opentrons.protocols.models import JsonProtocol as JsonProtocolUpToV5 +from opentrons.protocols.models.json_protocol import Model as JsonProtocolUpToV5 from .file_identifier import ( IdentifiedFile, diff --git a/api/src/opentrons/protocols/models/__init__.py b/api/src/opentrons/protocols/models/__init__.py index 62eccdf44ff..e69de29bb2d 100644 --- a/api/src/opentrons/protocols/models/__init__.py +++ b/api/src/opentrons/protocols/models/__init__.py @@ -1,21 +0,0 @@ -# Convenience re-exports of models that are especially common or important. -# More detailed sub-models are always available through the underlying -# submodules. -# -# If re-exporting something, its name should still make sense when it's separated -# from the name of its parent submodule. e.g. re-exporting models.json_protocol.Labware -# as models.Labware could be confusing. - -# TODO(mc, 2022-03-11): remove this re-export when it won't break pickling -# https://opentrons.atlassian.net/browse/RSS-94 -from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, - WellDefinition, -) -from .json_protocol import Model as JsonProtocol - -__all__ = [ - "LabwareDefinition", - "WellDefinition", - "JsonProtocol", -] diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index 7be480cfe0b..eaaf3f030d6 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -54,6 +54,11 @@ MixProperties, TouchTipProperties, BlowoutProperties, + MixParams, + LiquidClassTouchTipParams, + MultiDispenseProperties, + BlowoutParams, + BlowoutLocation, ) from opentrons_shared_data.deck.types import ( RobotModel, @@ -608,6 +613,7 @@ def minimal_labware_def() -> LabwareDefinition: "displayCategory": "other", "displayVolumeUnits": "mL", }, + "allowedRoles": ["labware"], "cornerOffsetFromSlot": {"x": 10, "y": 10, "z": 5}, "parameters": { "isTiprack": False, @@ -810,7 +816,7 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: tiprack="opentrons_flex_96_tiprack_50ul", aspirate=AspirateProperties( submerge=Submerge( - positionReference=PositionReference.LIQUID_MENISCUS, + positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=-5), speed=100, delay=DelayProperties( @@ -865,3 +871,144 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: ) ], ) + + +@pytest.fixture +def maximal_liquid_class_def() -> LiquidClassSchemaV1: + """Return a liquid class def with all properties enabled.""" + return LiquidClassSchemaV1( + liquidClassName="test_water", + displayName="Test Water", + schemaVersion=1, + namespace="opentrons", + byPipette=[ + ByPipetteSetting( + pipetteModel="flex_1channel_50", + byTipType=[ + ByTipTypeSetting( + tiprack="opentrons_flex_96_tiprack_50ul", + aspirate=AspirateProperties( + submerge=Submerge( + positionReference=PositionReference.WELL_TOP, + offset=Coordinate(x=1, y=2, z=3), + speed=100, + delay=DelayProperties( + enable=True, params=DelayParams(duration=10.0) + ), + ), + retract=RetractAspirate( + positionReference=PositionReference.WELL_TOP, + offset=Coordinate(x=3, y=2, z=1), + speed=50, + airGapByVolume=[(1.0, 0.1), (49.9, 0.1), (50.0, 0.0)], + touchTip=TouchTipProperties( + enable=True, + params=LiquidClassTouchTipParams( + zOffset=-1, mmToEdge=0.5, speed=30 + ), + ), + delay=DelayProperties( + enable=True, params=DelayParams(duration=20) + ), + ), + positionReference=PositionReference.WELL_BOTTOM, + offset=Coordinate(x=10, y=20, z=30), + flowRateByVolume=[(1.0, 35.0), (10.0, 24.0), (50.0, 35.0)], + correctionByVolume=[(0.0, 0.0)], + preWet=True, + mix=MixProperties( + enable=True, params=MixParams(repetitions=1, volume=50) + ), + delay=DelayProperties( + enable=True, params=DelayParams(duration=0.2) + ), + ), + singleDispense=SingleDispenseProperties( + submerge=Submerge( + positionReference=PositionReference.WELL_TOP, + offset=Coordinate(x=30, y=20, z=10), + speed=100, + delay=DelayProperties( + enable=True, params=DelayParams(duration=0.0) + ), + ), + retract=RetractDispense( + positionReference=PositionReference.WELL_TOP, + offset=Coordinate(x=11, y=22, z=33), + speed=50, + airGapByVolume=[(1.0, 0.1), (49.9, 0.1), (50.0, 0.0)], + blowout=BlowoutProperties( + enable=True, + params=BlowoutParams( + location=BlowoutLocation.SOURCE, + flowRate=100, + ), + ), + touchTip=TouchTipProperties( + enable=True, + params=LiquidClassTouchTipParams( + zOffset=-1, mmToEdge=0.5, speed=30 + ), + ), + delay=DelayProperties( + enable=True, params=DelayParams(duration=10) + ), + ), + positionReference=PositionReference.WELL_BOTTOM, + offset=Coordinate(x=33, y=22, z=11), + flowRateByVolume=[(1.0, 50.0)], + correctionByVolume=[(0.0, 0.0)], + mix=MixProperties( + enable=True, params=MixParams(repetitions=1, volume=50) + ), + pushOutByVolume=[ + (1.0, 7.0), + (4.999, 7.0), + (5.0, 2.0), + (10.0, 2.0), + (50.0, 2.0), + ], + delay=DelayProperties( + enable=True, params=DelayParams(duration=0.5) + ), + ), + multiDispense=MultiDispenseProperties( + submerge=Submerge( + positionReference=PositionReference.WELL_TOP, + offset=Coordinate(x=0, y=0, z=2), + speed=100, + delay=DelayProperties( + enable=False, params=DelayParams(duration=0.0) + ), + ), + retract=RetractDispense( + positionReference=PositionReference.WELL_TOP, + offset=Coordinate(x=2, y=3, z=1), + speed=50, + airGapByVolume=[(1.0, 0.1), (49.9, 0.1), (50.0, 0.0)], + blowout=BlowoutProperties(enable=False, params=None), + touchTip=TouchTipProperties( + enable=False, + params=LiquidClassTouchTipParams( + zOffset=-1, mmToEdge=0.5, speed=30 + ), + ), + delay=DelayProperties( + enable=False, params=DelayParams(duration=0) + ), + ), + positionReference=PositionReference.WELL_BOTTOM, + offset=Coordinate(x=1, y=3, z=2), + flowRateByVolume=[(50.0, 50.0)], + correctionByVolume=[(0.0, 0.0)], + conditioningByVolume=[(1.0, 5.0), (45.0, 5.0), (50.0, 0.0)], + disposalByVolume=[(1.0, 5.0), (45.0, 5.0), (50.0, 0.0)], + delay=DelayProperties( + enable=True, params=DelayParams(duration=0.2) + ), + ), + ) + ], + ) + ], + ) diff --git a/api/tests/opentrons/drivers/flex_stacker/test_driver.py b/api/tests/opentrons/drivers/flex_stacker/test_driver.py index aea2492cf9e..1de13c569cb 100644 --- a/api/tests/opentrons/drivers/flex_stacker/test_driver.py +++ b/api/tests/opentrons/drivers/flex_stacker/test_driver.py @@ -66,6 +66,27 @@ async def test_stop_motors(subject: FlexStackerDriver, connection: AsyncMock) -> await subject.get_device_info() +async def test_get_motion_params( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get motion params command.""" + connection.send_command.return_value = "M120 M:X V:200.000 A:1500.000 D:5.000" + response = await subject.get_motion_params(types.StackerAxis.X) + assert response == types.MoveParams( + axis=types.StackerAxis.X, + acceleration=1500.0, + max_speed=200.0, + max_speed_discont=5.0, + ) + + command = types.GCODE.GET_MOVE_PARAMS.build_command().add_element( + types.StackerAxis.X.name + ) + response = await connection.send_command(command) + connection.send_command.assert_any_call(command) + connection.reset_mock() + + async def test_set_serial_number( subject: FlexStackerDriver, connection: AsyncMock ) -> None: @@ -94,6 +115,32 @@ async def test_set_serial_number( connection.reset_mock() +async def test_enable_motors(subject: FlexStackerDriver, connection: AsyncMock) -> None: + """It should send a enable motors command""" + connection.send_command.return_value = "M17" + response = await subject.enable_motors([types.StackerAxis.X]) + assert response + + move_to = types.GCODE.ENABLE_MOTORS.build_command().add_element( + types.StackerAxis.X.value + ) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + # Test no arg to disable all motors + response = await subject.enable_motors(list(types.StackerAxis)) + assert response + + move_to = types.GCODE.ENABLE_MOTORS.build_command() + move_to.add_element(types.StackerAxis.X.value) + move_to.add_element(types.StackerAxis.Z.value) + move_to.add_element(types.StackerAxis.L.value) + + print("MOVE TO", move_to) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + async def test_get_limit_switch( subject: FlexStackerDriver, connection: AsyncMock ) -> None: diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index 9bd87fe62ec..d9901ac6cd8 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -145,7 +145,18 @@ def mock_messenger(can_message_notifier: MockCanMessageNotifier) -> mock.AsyncMo @pytest.fixture def mock_can_driver(mock_messenger: mock.AsyncMock) -> AbstractCanDriver: - return mock.AsyncMock(spec=AbstractCanDriver) + driver = mock.AsyncMock(spec=AbstractCanDriver) + + # ignoring this type error because this is a very weird function that will in fact + # do nothing, but has to have the yield in there for the compiler to make it a + # generator function + async def _fake_message_retrieve(d): # type: ignore[no-untyped-def] + while True: + await asyncio.sleep(1) + yield + + driver.__aiter__ = _fake_message_retrieve + return driver @pytest.fixture @@ -160,15 +171,19 @@ def mock_eeprom_driver() -> EEPROMDriver: @pytest.fixture -def controller( +async def controller( mock_config: OT3Config, mock_can_driver: AbstractCanDriver, mock_eeprom_driver: EEPROMDriver, -) -> OT3Controller: - with (mock.patch("opentrons.hardware_control.backends.ot3controller.OT3GPIO")): - return OT3Controller( +) -> AsyncIterator[OT3Controller]: + with mock.patch("opentrons.hardware_control.backends.ot3controller.OT3GPIO"): + controller = OT3Controller( mock_config, mock_can_driver, eeprom_driver=mock_eeprom_driver ) + try: + yield controller + finally: + await controller.clean_up() @pytest.fixture diff --git a/api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py b/api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py new file mode 100644 index 00000000000..0a6d43e441f --- /dev/null +++ b/api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py @@ -0,0 +1,52 @@ +import asyncio +import pytest +import mock +from typing import AsyncGenerator +from opentrons.hardware_control import modules, ExecutionManager +from opentrons.drivers.rpi_drivers.types import USBPort + + +@pytest.fixture +def usb_port() -> USBPort: + return USBPort( + name="", + port_number=0, + device_path="/dev/ot_module_sim_flexstacker0", + ) + + +@pytest.fixture +async def simulating_module( + usb_port: USBPort, +) -> AsyncGenerator[modules.AbstractModule, None]: + module = await modules.build( + port=usb_port.device_path, + usb_port=usb_port, + type=modules.ModuleType["FLEX_STACKER"], + simulating=True, + hw_control_loop=asyncio.get_running_loop(), + execution_manager=ExecutionManager(), + ) + assert isinstance(module, modules.AbstractModule) + try: + yield module + finally: + await module.cleanup() + + +@pytest.fixture +async def simulating_module_driver_patched( + simulating_module: modules.FlexStacker, +) -> AsyncGenerator[modules.AbstractModule, None]: + driver_mock = mock.MagicMock() + with mock.patch.object( + simulating_module, "_driver", driver_mock + ), mock.patch.object(simulating_module._reader, "_driver", driver_mock): + yield simulating_module + + +async def test_sim_state(simulating_module: modules.FlexStacker) -> None: + status = simulating_module.device_info + assert status["serial"] == "dummySerialFS" + assert status["model"] == "a1" + assert status["version"] == "stacker-fw" diff --git a/api/tests/opentrons/hardware_control/modules/test_hc_tempdeck.py b/api/tests/opentrons/hardware_control/modules/test_hc_tempdeck.py index 94953a496c4..51c99966d06 100644 --- a/api/tests/opentrons/hardware_control/modules/test_hc_tempdeck.py +++ b/api/tests/opentrons/hardware_control/modules/test_hc_tempdeck.py @@ -42,8 +42,11 @@ async def test_sim_state(subject: modules.AbstractModule) -> None: assert subject.target is None assert subject.status == "idle" assert subject.live_data["status"] == subject.status - assert subject.live_data["data"]["currentTemp"] == subject.temperature - assert subject.live_data["data"]["targetTemp"] == subject.target + + live_data = subject.live_data["data"] + assert modules.ModuleDataValidator.is_temperature_module_data(live_data) + assert live_data["currentTemp"] == subject.temperature + assert live_data["targetTemp"] == subject.target status = subject.device_info assert status["serial"] == "dummySerialTD" # return v1 if sim_model is not passed diff --git a/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py b/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py index 6e90068ac1f..23389b9590f 100644 --- a/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py +++ b/api/tests/opentrons/hardware_control/modules/test_hc_thermocycler.py @@ -126,8 +126,10 @@ async def test_sim_state(subject: modules.Thermocycler) -> None: assert subject.target is None assert subject.status == "idle" assert subject.live_data["status"] == subject.status - assert subject.live_data["data"]["currentTemp"] == subject.temperature - assert subject.live_data["data"]["targetTemp"] == subject.target + live_data = subject.live_data["data"] + assert modules.ModuleDataValidator.is_thermocycler_data(live_data) + assert live_data["currentTemp"] == subject.temperature + assert live_data["targetTemp"] == subject.target status = subject.device_info assert status["serial"] == "dummySerialTC" assert status["model"] == "dummyModelTC" diff --git a/api/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index 5df0b142e07..f5fa0e69336 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -8,6 +8,7 @@ from opentrons.hardware_control import ExecutionManager from opentrons.hardware_control.modules import ModuleAtPort +from opentrons.hardware_control.modules.flex_stacker import FlexStacker from opentrons.hardware_control.modules.types import ( BundledFirmware, ModuleModel, @@ -16,6 +17,7 @@ HeaterShakerModuleModel, ThermocyclerModuleModel, AbsorbanceReaderModel, + FlexStackerModuleModel, ModuleType, ) from opentrons.hardware_control.modules import ( @@ -49,6 +51,9 @@ async def test_get_modules_simulating() -> None: "absorbancereader": [ SimulatingModule(serial_number="555", model="absorbanceReaderV1") ], + "flexstacker": [ + SimulatingModule(serial_number="656", model="flexStackerModuleV1") + ], } api = await hardware_control.API.build_hardware_simulator(attached_modules=mods) await asyncio.sleep(0.05) @@ -110,6 +115,7 @@ async def test_module_caching() -> None: (ThermocyclerModuleModel.THERMOCYCLER_V1, Thermocycler), (HeaterShakerModuleModel.HEATER_SHAKER_V1, HeaterShaker), (AbsorbanceReaderModel.ABSORBANCE_READER_V1, AbsorbanceReader), + (FlexStackerModuleModel.FLEX_STACKER_V1, FlexStacker), ], ) async def test_create_simulating_module( @@ -259,7 +265,28 @@ async def mod_absorbancereader() -> AsyncIterator[AbstractModule]: await absorbancereader.cleanup() -async def test_module_update_integration( +@pytest.fixture +async def mod_flexstacker() -> AsyncIterator[AbstractModule]: + usb_port = USBPort( + name="", + hub=False, + port_number=0, + device_path="/dev/ot_module_sim_flexstacker0", + ) + + flexstacker = await build_module( + port="/dev/ot_module_sim_flexstacker0", + usb_port=usb_port, + type=ModuleType.FLEX_STACKER, + simulating=True, + hw_control_loop=asyncio.get_running_loop(), + execution_manager=ExecutionManager(), + ) + yield flexstacker + await flexstacker.cleanup() + + +async def test_module_update_integration( # noqa: C901 monkeypatch: pytest.MonkeyPatch, mod_tempdeck: AbstractModule, mod_magdeck: AbstractModule, @@ -267,6 +294,7 @@ async def test_module_update_integration( mod_heatershaker: AbstractModule, mod_thermocycler_gen2: AbstractModule, mod_absorbancereader: AbstractModule, + mod_flexstacker: AbstractModule, ) -> None: from opentrons.hardware_control import modules @@ -362,6 +390,7 @@ async def mock_find_dfu_device_tc2(pid: str, expected_device_count: int) -> str: upload_via_dfu_mock.assert_called_once_with( "df11", "fake_fw_file_path", bootloader_kwargs ) + upload_via_dfu_mock.reset_mock() # Test absorbancereader update with byonoy library bootloader_kwargs["module"] = mod_absorbancereader @@ -373,6 +402,20 @@ async def mock_find_dfu_device_tc2(pid: str, expected_device_count: int) -> str: byonoy_update_firmware_mock.assert_called_once_with("fake_fw_file_path") assert not mod_absorbancereader.updating + # test flex stacker update with dfu bootloader + async def mock_find_dfu_device_fs2(pid: str, expected_device_count: int) -> str: + if expected_device_count == 3: + return "df11" + return "none" + + monkeypatch.setattr(modules.update, "find_dfu_device", mock_find_dfu_device_fs2) + + bootloader_kwargs["module"] = mod_flexstacker + await modules.update_firmware(mod_flexstacker, "fake_fw_file_path") + upload_via_dfu_mock.assert_called_once_with( + "df11", "fake_fw_file_path", bootloader_kwargs + ) + async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> None: from opentrons.hardware_control import modules @@ -392,6 +435,9 @@ async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> dummy_abs_file = Path(tmpdir) / "absorbance-96@v1.0.2.byoup" dummy_abs_file.write_text("hello") + dummy_fs_file = Path(tmpdir) / "flex-stacker@v7.0.0.bin" + dummy_fs_file.write_text("hello") + dummy_bogus_file = Path(tmpdir) / "thermoshaker@v6.6.6.bin" dummy_bogus_file.write_text("hello") @@ -414,6 +460,9 @@ async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> "absorbancereader": [ SimulatingModule(serial_number="555", model="absorbanceReaderV1") ], + "flexstacker": [ + SimulatingModule(serial_number="656", model="flexStackerModuleV1") + ], } api = await API.build_hardware_simulator(attached_modules=mods) @@ -434,6 +483,9 @@ async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> assert api.attached_modules[4].bundled_fw == BundledFirmware( version="1.0.2", path=dummy_abs_file ) + assert api.attached_modules[5].bundled_fw == BundledFirmware( + version="7.0.0", path=dummy_fs_file + ) for m in api.attached_modules: await m.cleanup() diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 73f39006299..d92e3d6050a 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -8,6 +8,8 @@ from decoy import errors from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, + PositionReference, + Coordinate, ) from opentrons_shared_data.pipette.types import PipetteNameType @@ -15,6 +17,13 @@ from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocol_api._liquid_properties import TransferProperties +from opentrons.protocol_api.core.engine import transfer_components_executor, LabwareCore +from opentrons.protocol_api.core.engine.transfer_components_executor import ( + TransferComponentsExecutor, + TransferType, + TipState, + LiquidAndAirGapPair, +) from opentrons.protocol_engine import ( DeckPoint, LoadedPipette, @@ -30,6 +39,7 @@ ) from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients.sync_client import SyncClient +from opentrons.protocol_engine.commands import GetNextTipResult from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_engine.types import ( @@ -41,6 +51,9 @@ ColumnNozzleLayoutConfiguration, AddressableOffsetVector, LiquidClassRecord, + NextTipInfo, + NoTipAvailable, + NoTipReason, ) from opentrons.protocol_api.disposal_locations import ( TrashBin, @@ -57,6 +70,7 @@ ) from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.advanced_control.transfers import common as tx_commons from opentrons.types import Location, Mount, MountType, Point, NozzleConfigurationType from ... import versions_below, versions_at_or_above @@ -91,6 +105,53 @@ def patch_mock_pipette_movement_safety_check( ) +@pytest.fixture(autouse=True) +def patch_mock_check_valid_volume_parameters( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Replace tx_commons.check_valid_volume_parameters() with a mock.""" + mock = decoy.mock(func=tx_commons.check_valid_volume_parameters) + monkeypatch.setattr(tx_commons, "check_valid_volume_parameters", mock) + + +@pytest.fixture(autouse=True) +def patch_mock_expand_for_volume_constraints( + decoy: Decoy, monkeypatch: pytest.MonkeyPatch +) -> None: + """Replace tx_commons.expand_for_volume_constraints() with a mock.""" + mock = decoy.mock(func=tx_commons.expand_for_volume_constraints) + monkeypatch.setattr(tx_commons, "expand_for_volume_constraints", mock) + + +@pytest.fixture +def mock_transfer_components_executor( + decoy: Decoy, +) -> TransferComponentsExecutor: + """Get a mocked out TransferComponentsExecutor.""" + return decoy.mock(cls=TransferComponentsExecutor) + + +@pytest.fixture(autouse=True) +def patch_mock_transfer_components_executor( + decoy: Decoy, + monkeypatch: pytest.MonkeyPatch, + mock_transfer_components_executor: TransferComponentsExecutor, +) -> None: + """Replace transfer_components_executor functions with mocks.""" + monkeypatch.setattr( + transfer_components_executor, + "TransferComponentsExecutor", + mock_transfer_components_executor, + ) + monkeypatch.setattr( + transfer_components_executor, + "absolute_point_from_position_reference_and_offset", + decoy.mock( + func=transfer_components_executor.absolute_point_from_position_reference_and_offset + ), + ) + + @pytest.fixture def subject( decoy: Decoy, @@ -139,6 +200,21 @@ def test_get_pipette_name( assert result == "p300_single" +def test_get_pipette_load_name( + decoy: Decoy, mock_engine_client: EngineClient, subject: InstrumentCore +) -> None: + """It should get the pipette's API-specific load name.""" + decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( + LoadedPipette.model_construct(pipetteName=PipetteNameType.P300_SINGLE) # type: ignore[call-arg] + ) + assert subject.get_load_name() == "p300_single" + + decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( + LoadedPipette.model_construct(pipetteName=PipetteNameType.P1000_96) # type: ignore[call-arg] + ) + assert subject.get_load_name() == "flex_96channel_1000" + + def test_get_mount( decoy: Decoy, mock_engine_client: EngineClient, subject: InstrumentCore ) -> None: @@ -1521,6 +1597,9 @@ def test_load_liquid_class( test_liq_class = decoy.mock(cls=LiquidClass) test_transfer_props = decoy.mock(cls=TransferProperties) + decoy.when(mock_engine_client.state.pipettes.get("abc123")).then_return( + LoadedPipette.model_construct(pipetteName=PipetteNameType.P50_SINGLE_FLEX) # type: ignore[call-arg] + ) decoy.when( test_liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") ).then_return(test_transfer_props) @@ -1552,8 +1631,184 @@ def test_load_liquid_class( ) ).then_return(cmd.LoadLiquidClassResult(liquidClassId="liquid-class-id")) result = subject.load_liquid_class( - liquid_class=test_liq_class, - pipette_load_name="flex_1channel_50", + name=test_liq_class.name, + transfer_properties=test_transfer_props, tiprack_uri="opentrons_flex_96_tiprack_50ul", ) assert result == "liquid-class-id" + + +def test_aspirate_liquid_class( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, + minimal_liquid_class_def2: LiquidClassSchemaV1, + mock_transfer_components_executor: TransferComponentsExecutor, +) -> None: + """It should call aspirate sub-steps execution based on liquid class.""" + source_well = decoy.mock(cls=WellCore) + source_location = Location(Point(1, 2, 3), labware=None) + test_liquid_class = LiquidClass.create(minimal_liquid_class_def2) + test_transfer_properties = test_liquid_class.get_for( + "flex_1channel_50", "opentrons_flex_96_tiprack_50ul" + ) + decoy.when( + transfer_components_executor.absolute_point_from_position_reference_and_offset( + well=source_well, + position_reference=PositionReference.WELL_BOTTOM, + offset=Coordinate(x=0, y=0, z=-5), + ) + ).then_return(Point(1, 2, 3)) + decoy.when( + transfer_components_executor.TransferComponentsExecutor( + instrument_core=subject, + transfer_properties=test_transfer_properties, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + transfer_type=TransferType.ONE_TO_ONE, + tip_state=TipState(), + ) + ).then_return(mock_transfer_components_executor) + decoy.when( + mock_transfer_components_executor.tip_state.last_liquid_and_air_gap_in_tip + ).then_return(LiquidAndAirGapPair(liquid=111, air_gap=222)) + result = subject.aspirate_liquid_class( + volume=123, + source=(source_location, source_well), + transfer_properties=test_transfer_properties, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[], + ) + decoy.verify( + mock_transfer_components_executor.submerge( + submerge_properties=test_transfer_properties.aspirate.submerge, + ), + mock_transfer_components_executor.mix( + mix_properties=test_transfer_properties.aspirate.mix, + last_dispense_push_out=False, + ), + mock_transfer_components_executor.pre_wet(volume=123), + mock_transfer_components_executor.aspirate_and_wait(volume=123), + mock_transfer_components_executor.retract_after_aspiration(volume=123), + ) + assert result == [LiquidAndAirGapPair(air_gap=222, liquid=111)] + + +def test_dispense_liquid_class( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, + minimal_liquid_class_def2: LiquidClassSchemaV1, + mock_transfer_components_executor: TransferComponentsExecutor, +) -> None: + """It should call dispense sub-steps execution based on liquid class.""" + source_well = decoy.mock(cls=WellCore) + source_location = Location(Point(1, 2, 3), labware=None) + dest_well = decoy.mock(cls=WellCore) + dest_location = Location(Point(3, 2, 1), labware=None) + test_liquid_class = LiquidClass.create(minimal_liquid_class_def2) + test_transfer_properties = test_liquid_class.get_for( + "flex_1channel_50", "opentrons_flex_96_tiprack_50ul" + ) + push_out_vol = test_transfer_properties.dispense.push_out_by_volume.get_for_volume( + 123 + ) + decoy.when( + transfer_components_executor.absolute_point_from_position_reference_and_offset( + well=dest_well, + position_reference=PositionReference.WELL_BOTTOM, + offset=Coordinate(x=0, y=0, z=-5), + ) + ).then_return(Point(1, 2, 3)) + decoy.when( + transfer_components_executor.TransferComponentsExecutor( + instrument_core=subject, + transfer_properties=test_transfer_properties, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=dest_well, + transfer_type=TransferType.ONE_TO_ONE, + tip_state=TipState(), + ) + ).then_return(mock_transfer_components_executor) + decoy.when( + mock_transfer_components_executor.tip_state.last_liquid_and_air_gap_in_tip + ).then_return(LiquidAndAirGapPair(liquid=333, air_gap=444)) + result = subject.dispense_liquid_class( + volume=123, + dest=(dest_location, dest_well), + source=(source_location, source_well), + transfer_properties=test_transfer_properties, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[], + add_final_air_gap=True, + trash_location=Location(Point(1, 2, 3), labware=None), + ) + decoy.verify( + mock_transfer_components_executor.submerge( + submerge_properties=test_transfer_properties.dispense.submerge, + ), + mock_transfer_components_executor.dispense_and_wait( + volume=123, + push_out_override=push_out_vol, + ), + mock_transfer_components_executor.mix( + mix_properties=test_transfer_properties.dispense.mix, + last_dispense_push_out=True, + ), + mock_transfer_components_executor.retract_after_dispensing( + trash_location=Location(Point(1, 2, 3), labware=None), + source_location=source_location, + source_well=source_well, + add_final_air_gap=True, + ), + ) + assert result == [LiquidAndAirGapPair(air_gap=444, liquid=333)] + + +def test_get_next_tip( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, +) -> None: + """It should return the next tip result.""" + tip_racks = [decoy.mock(cls=LabwareCore)] + expected_next_tip = NextTipInfo(labwareId="1234", tipStartingWell="BAR") + decoy.when(tip_racks[0].labware_id).then_return("tiprack-id") + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.GetNextTipParams( + pipetteId="abc123", labwareIds=["tiprack-id"], startingTipWell="F00" + ) + ) + ).then_return(GetNextTipResult(nextTipInfo=expected_next_tip)) + result = subject.get_next_tip( + tip_racks=tip_racks, + starting_well="F00", + ) + assert result == expected_next_tip + + +def test_get_next_tip_when_no_tip_available( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, +) -> None: + """It should return None when there's no next tip available.""" + tip_racks = [decoy.mock(cls=LabwareCore)] + decoy.when(tip_racks[0].labware_id).then_return("tiprack-id") + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.GetNextTipParams( + pipetteId="abc123", labwareIds=["tiprack-id"], startingTipWell="F00" + ) + ) + ).then_return( + GetNextTipResult( + nextTipInfo=NoTipAvailable(noTipReason=NoTipReason.NO_AVAILABLE_TIPS) + ) + ) + result = subject.get_next_tip( + tip_racks=tip_racks, + starting_well="F00", + ) + assert result is None diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 6b5065f98c9..2889a47cea9 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -756,6 +756,154 @@ def test_load_adapter_on_staging_slot( assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result +def test_load_lid( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: ProtocolCore, +) -> None: + """It should issue a LoadLid command.""" + mock_labware_core = decoy.mock(cls=LabwareCore) + decoy.when(mock_labware_core.labware_id).then_return("labware-id") + decoy.when( + mock_engine_client.state.labware.find_custom_labware_load_params() + ).then_return([EngineLabwareLoadParams("hello", "world", 654)]) + + decoy.when( + load_labware_params.resolve( + "some_labware", + "a_namespace", + 456, + [EngineLabwareLoadParams("hello", "world", 654)], + ) + ).then_return(("some_namespace", 9001)) + + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.LoadLidParams( + location=OnLabwareLocation(labwareId="labware-id"), + loadName="some_labware", + namespace="some_namespace", + version=9001, + ) + ) + ).then_return( + commands.LoadLidResult( + labwareId="abc123", + definition=LabwareDefinition.model_construct(ordering=[]), # type: ignore[call-arg] + ) + ) + + decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + ) + + result = subject.load_lid( + load_name="some_labware", + location=mock_labware_core, + namespace="a_namespace", + version=456, + ) + + assert isinstance(result, LabwareCore) + assert result.labware_id == "abc123" + assert subject.get_labware_cores() == [result] + + decoy.verify( + deck_conflict.check( + engine_state=mock_engine_client.state, + existing_labware_ids=[], + existing_module_ids=[], + existing_disposal_locations=[], + new_labware_id="abc123", + ) + ) + + decoy.when( + mock_engine_client.state.geometry.get_slot_item( + slot_name=DeckSlotName.SLOT_5, + ) + ).then_return( + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] + ) + + assert subject.get_slot_item(DeckSlotName.SLOT_5) is result + + +def test_load_lid_stack( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: ProtocolCore, +) -> None: + """It should issue a LoadLidStack command.""" + decoy.when( + mock_engine_client.state.labware.find_custom_labware_load_params() + ).then_return([EngineLabwareLoadParams("hello", "world", 654)]) + + decoy.when( + load_labware_params.resolve( + "some_labware", + "a_namespace", + 456, + [EngineLabwareLoadParams("hello", "world", 654)], + ) + ).then_return(("some_namespace", 9001)) + + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.LoadLidStackParams( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), + loadName="some_labware", + namespace="some_namespace", + version=9001, + quantity=5, + ) + ) + ).then_return( + commands.LoadLidStackResult( + stackLabwareId="abc123", + labwareIds=["1", "2", "3", "4", "5"], + definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), + ) + ) + + decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( + LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + ) + + result = subject.load_lid_stack( + load_name="some_labware", + location=DeckSlotName.SLOT_5, + namespace="a_namespace", + version=456, + quantity=5, + ) + + assert isinstance(result, LabwareCore) + assert result.labware_id == "abc123" + assert subject.get_labware_cores() == [result] + + decoy.verify( + deck_conflict.check( + engine_state=mock_engine_client.state, + existing_labware_ids=[], + existing_module_ids=[], + existing_disposal_locations=[], + new_labware_id="abc123", + ) + ) + + decoy.when( + mock_engine_client.state.geometry.get_slot_item( + slot_name=DeckSlotName.SLOT_5, + ) + ).then_return( + LoadedLabware.model_construct(id="abc123") # type: ignore[call-arg] + ) + + assert subject.get_slot_item(DeckSlotName.SLOT_5) is result + + def test_load_trash_bin( decoy: Decoy, mock_engine_client: EngineClient, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py new file mode 100644 index 00000000000..6eecfccf882 --- /dev/null +++ b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py @@ -0,0 +1,979 @@ +"""Tests for complex commands executor.""" +import pytest +from decoy import Decoy +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, + PositionReference, + Coordinate, + BlowoutLocation, +) + +from opentrons.protocol_api import TrashBin +from opentrons.protocol_api._liquid import LiquidClass +from opentrons.protocol_api._liquid_properties import TransferProperties +from opentrons.protocol_api.core.engine.well import WellCore +from opentrons.protocol_api.core.engine.instrument import InstrumentCore +from opentrons.protocol_api.core.engine.transfer_components_executor import ( + TransferComponentsExecutor, + absolute_point_from_position_reference_and_offset, + TipState, + TransferType, + LiquidAndAirGapPair, +) +from opentrons.types import Location, Point +from opentrons.protocol_api.labware import Well + + +@pytest.fixture +def mock_instrument_core(decoy: Decoy) -> InstrumentCore: + """Return a mocked out instrument core.""" + return decoy.mock(cls=InstrumentCore) + + +@pytest.fixture +def sample_transfer_props( + maximal_liquid_class_def: LiquidClassSchemaV1, +) -> TransferProperties: + """Return a mocked out liquid class fixture.""" + return LiquidClass.create(maximal_liquid_class_def).get_for( + pipette="flex_1channel_50", tiprack="opentrons_flex_96_tiprack_50ul" + ) + + +""" Test aspirate properties: +"submerge": { + "positionReference": "well-top", + "offset": {"x": 1, "y": 2, "z": 3}, + "speed": 100, + "delay": {"enable": true, "params": {"duration": 10.0}}}, +"retract": { + "positionReference": "well-top", + "offset": {"x": 3, "y": 2, "z": 1}, + "speed": 50, + "airGapByVolume": [[1.0, 0.1], [49.9, 0.1], [50.0, 0.0]], + "touchTip": {"enable": true, "params": {"zOffset": -1, "mmToEdge": 0.5, "speed": 30}}, + "delay": {"enable": true, "params": {"duration": 20}}}, +"positionReference": "well-bottom", +"offset": {"x": 10, "y": 20, "z": 30}, +"flowRateByVolume": [[1.0, 35.0], [10.0, 24.0], [50.0, 35.0]], +"correctionByVolume": [[0.0, 0.0]], +"preWet": true, +"mix": {"enable": true, "params": {"repetitions": 1, "volume": 50}}, +"delay": {"enable": true, "params": {"duration": 0.2}} +""" + + +def test_submerge( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """Should perform the expected submerge steps.""" + source_well = decoy.mock(cls=WellCore) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + air_gap_removal_flow_rate = ( + sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(123) + ) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(), labware=None), + target_well=source_well, + tip_state=TipState( + ready_to_aspirate=True, + last_liquid_and_air_gap_in_tip=LiquidAndAirGapPair(liquid=0, air_gap=123), + ), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(source_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(source_well.get_top(0)).then_return(well_top_point) + + subject.submerge(submerge_properties=sample_transfer_props.aspirate.submerge) + + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(x=2, y=4, z=6), labware=None), + well_core=source_well, + force_direct=False, + minimum_z_height=None, + speed=None, + ), + mock_instrument_core.dispense( + location=Location(Point(x=2, y=4, z=6), labware=None), + well_core=None, + volume=123, + rate=1, + flow_rate=air_gap_removal_flow_rate, + in_place=True, + is_meniscus=None, + push_out=0, + ), + mock_instrument_core.delay(0.5), + mock_instrument_core.move_to( + location=Location(Point(), labware=None), + well_core=source_well, + force_direct=True, + minimum_z_height=None, + speed=100, + ), + mock_instrument_core.delay(10), + ) + + +def test_aspirate_and_wait( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should execute an aspirate and a delay according to properties.""" + source_well = decoy.mock(cls=WellCore) + aspirate_flow_rate = ( + sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(10) + ) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.aspirate_and_wait(volume=10) + decoy.verify( + mock_instrument_core.aspirate( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=10, + rate=1, + flow_rate=aspirate_flow_rate, + in_place=True, + is_meniscus=None, + ), + mock_instrument_core.delay(0.2), + ) + + +def test_aspirate_and_wait_skips_delay( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should skip the wait after aspirate.""" + sample_transfer_props.aspirate.delay.enabled = False + source_well = decoy.mock(cls=WellCore) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.aspirate_and_wait(volume=10) + decoy.verify( + mock_instrument_core.delay(0.2), + times=0, + ) + + +def test_dispense_and_wait( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should execute a dispense and a delay according to properties.""" + source_well = decoy.mock(cls=WellCore) + dispense_flow_rate = ( + sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(10) + ) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.dispense_and_wait(volume=10, push_out_override=123) + decoy.verify( + mock_instrument_core.dispense( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=10, + rate=1, + flow_rate=dispense_flow_rate, + in_place=True, + push_out=123, + is_meniscus=None, + ), + mock_instrument_core.delay(0.5), + ) + + +def test_dispense_and_wait_skips_delay( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should skip the wait after dispense.""" + sample_transfer_props.dispense.delay.enabled = False + source_well = decoy.mock(cls=WellCore) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.dispense_and_wait(volume=10, push_out_override=123) + decoy.verify( + mock_instrument_core.delay(0.2), + times=0, + ) + + +def test_mix( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should execute mix steps.""" + source_well = decoy.mock(cls=WellCore) + aspirate_flow_rate = ( + sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(50) + ) + dispense_flow_rate = ( + sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(50) + ) + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.mix( + mix_properties=sample_transfer_props.aspirate.mix, + last_dispense_push_out=True, + ) + + decoy.verify( + mock_instrument_core.aspirate( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=50, + rate=1, + flow_rate=aspirate_flow_rate, + in_place=True, + is_meniscus=None, + ), + mock_instrument_core.delay(0.2), + mock_instrument_core.dispense( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=50, + rate=1, + flow_rate=dispense_flow_rate, + in_place=True, + push_out=2.0, + is_meniscus=None, + ), + mock_instrument_core.delay(0.5), + ) + + +def test_mix_disabled( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should not perform a mix when it is disabled.""" + sample_transfer_props.aspirate.mix.enabled = False + source_well = decoy.mock(cls=WellCore) + aspirate_flow_rate = ( + sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(50) + ) + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.mix( + mix_properties=sample_transfer_props.aspirate.mix, + last_dispense_push_out=True, + ) + decoy.verify( + mock_instrument_core.aspirate( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=50, + rate=1, + flow_rate=aspirate_flow_rate, + in_place=True, + is_meniscus=None, + ), + times=0, + ) + + +def test_pre_wet( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should execute pre-wet steps.""" + source_well = decoy.mock(cls=WellCore) + aspirate_flow_rate = ( + sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(40) + ) + dispense_flow_rate = ( + sample_transfer_props.dispense.flow_rate_by_volume.get_for_volume(40) + ) + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.pre_wet(volume=40) + + decoy.verify( + mock_instrument_core.aspirate( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=40, + rate=1, + flow_rate=aspirate_flow_rate, + in_place=True, + is_meniscus=None, + ), + mock_instrument_core.delay(0.2), + mock_instrument_core.dispense( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=40, + rate=1, + flow_rate=dispense_flow_rate, + in_place=True, + push_out=0, + is_meniscus=None, + ), + mock_instrument_core.delay(0.5), + ) + + +def test_pre_wet_disabled( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should NOT execute pre-wet steps.""" + source_well = decoy.mock(cls=WellCore) + sample_transfer_props.aspirate.pre_wet = False + aspirate_flow_rate = ( + sample_transfer_props.aspirate.flow_rate_by_volume.get_for_volume(40) + ) + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + subject.pre_wet(volume=40) + + decoy.verify( + mock_instrument_core.aspirate( + location=Location(Point(1, 2, 3), labware=None), + well_core=None, + volume=40, + rate=1, + flow_rate=aspirate_flow_rate, + in_place=True, + is_meniscus=None, + ), + times=0, + ) + + +def test_retract_after_aspiration( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should execute steps to retract from well after an aspiration.""" + source_well = decoy.mock(cls=WellCore) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + + air_gap_volume = ( + sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(40) + ) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 1, 1), labware=None), + target_well=source_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(source_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(source_well.get_top(0)).then_return(well_top_point) + + subject.retract_after_aspiration(volume=40) + + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(x=4, y=4, z=4), labware=None), + well_core=source_well, + force_direct=True, + minimum_z_height=None, + speed=50, + ), + mock_instrument_core.delay(20), + mock_instrument_core.touch_tip( + location=Location(Point(x=4, y=4, z=4), labware=None), + well_core=source_well, + radius=1, # Update this to use mmToEdge once implemented + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=Location(Point(x=4, y=4, z=4), labware=None), + well_core=source_well, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + mock_instrument_core.air_gap_in_place( + volume=air_gap_volume, + flow_rate=air_gap_volume, + ), + mock_instrument_core.delay(0.2), + ) + + +def test_retract_after_aspiration_without_touch_tip_and_delay( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, +) -> None: + """It should execute steps to retract from well after an aspiration without a touch tip or delay.""" + source_well = decoy.mock(cls=WellCore) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + + sample_transfer_props.aspirate.retract.touch_tip.enabled = False + sample_transfer_props.aspirate.retract.delay.enabled = False + + air_gap_volume = ( + sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(40) + ) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 1, 1), labware=None), + target_well=source_well, + tip_state=TipState( + ready_to_aspirate=True, + last_liquid_and_air_gap_in_tip=LiquidAndAirGapPair( + liquid=10, + air_gap=0, + ), + ), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(source_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(source_well.get_top(0)).then_return(well_top_point) + + subject.retract_after_aspiration(volume=40) + + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(x=4, y=4, z=4), labware=None), + well_core=source_well, + force_direct=True, + minimum_z_height=None, + speed=50, + ), + mock_instrument_core.air_gap_in_place( + volume=air_gap_volume, + flow_rate=air_gap_volume, + ), + mock_instrument_core.delay(0.2), + ) + + +""" +Single dispense properties: + +"singleDispense": { + "submerge": { + "positionReference": "well-top", + "offset": {"x": 30, "y": 20, "z": 10}, + "speed": 100, + "delay": {"enable": true, "params": { "duration": 0.0 }} + }, + "retract": { + "positionReference": "well-top", + "offset": {"x": 11, "y": 22, "z": 33}, + "speed": 50, + "airGapByVolume": [[1.0, 0.1], [49.9, 0.1], [50.0, 0.0]], + "blowout": { "enable": true , "params": {"location": "source", "flowRate": 100}}, + "touchTip": { "enable": true, "params": { "zOffset": -1, "mmToEdge": 0.5, "speed": 30}}, + "delay": {"enable": true, "params": { "duration": 10 }} + }, + "positionReference": "well-bottom", + "offset": {"x": 33, "y": 22, "z": 11}, + "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], + "mix": { "enable": true, "params": { "repetitions": 1, "volume": 50 }}, + "pushOutByVolume": [[1.0, 7.0], [4.999, 7.0], [5.0, 2.0], [10.0, 2.0], [50.0, 2.0]], + "delay": { "enable": true, "params": { "duration": 0.2 }}, +} +""" + + +@pytest.mark.parametrize( + "add_final_air_gap", + [True, False], +) +def test_retract_after_dispense_with_blowout_in_source( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, + add_final_air_gap: bool, +) -> None: + """It should execute steps to retract from well after a dispense.""" + source_location = Location(Point(1, 2, 3), labware=None) + source_well = decoy.mock(cls=WellCore) + dest_well = decoy.mock(cls=WellCore) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + air_gap_volume = ( + sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) + ) + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 1, 1), labware=None), + target_well=dest_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(dest_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(dest_well.get_top(0)).then_return(well_top_point) + decoy.when(source_well.get_top(0)).then_return(Point(10, 20, 30)) + subject.retract_after_dispensing( + trash_location=Location(Point(), labware=None), + source_location=source_location, + source_well=source_well, + add_final_air_gap=add_final_air_gap, + ) + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=50, + ), + mock_instrument_core.delay(10), + mock_instrument_core.touch_tip( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + radius=1, + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + mock_instrument_core.air_gap_in_place( + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), + mock_instrument_core.set_flow_rate(blow_out=100), + mock_instrument_core.blow_out( + location=Location(Point(10, 20, 30), labware=None), + well_core=source_well, + in_place=False, + ), + mock_instrument_core.touch_tip( + location=Location(Point(10, 20, 30), labware=None), + well_core=source_well, + radius=1, + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=Location(Point(10, 20, 30), labware=None), + well_core=source_well, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + mock_instrument_core.prepare_to_aspirate(), + *( + add_final_air_gap + and [ + mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] + ] + or [] + ), + ) + + +@pytest.mark.parametrize( + "add_final_air_gap", + [True, False], +) +def test_retract_after_dispense_with_blowout_in_destination( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, + add_final_air_gap: bool, +) -> None: + """It should execute steps to retract from well after a dispense.""" + source_well = decoy.mock(cls=WellCore) + dest_well = decoy.mock(cls=WellCore) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + air_gap_volume = ( + sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) + ) + sample_transfer_props.dispense.retract.blowout.location = ( + BlowoutLocation.DESTINATION + ) + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 1, 1), labware=None), + target_well=dest_well, + tip_state=TipState( + ready_to_aspirate=True, + last_liquid_and_air_gap_in_tip=LiquidAndAirGapPair( + liquid=10, + air_gap=0, + ), + ), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(dest_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(dest_well.get_top(0)).then_return(well_top_point) + + subject.retract_after_dispensing( + trash_location=Location(Point(), labware=None), + source_location=Location(Point(1, 2, 3), labware=None), + source_well=source_well, + add_final_air_gap=True, + ) + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=50, + ), + mock_instrument_core.delay(10), + mock_instrument_core.set_flow_rate(blow_out=100), + mock_instrument_core.blow_out( + location=Location(Point(12, 24, 36), labware=None), + well_core=None, + in_place=True, + ), + mock_instrument_core.touch_tip( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + radius=1, + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + *( + add_final_air_gap + and [ + mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] + ] + or [] + ), + ) + + +@pytest.mark.parametrize( + "add_final_air_gap", + [True, False], +) +def test_retract_after_dispense_with_blowout_in_trash_well( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, + add_final_air_gap: bool, +) -> None: + """It should execute steps to retract from well after a dispense.""" + source_location = Location(Point(1, 2, 3), labware=None) + source_well = decoy.mock(cls=WellCore) + dest_well = decoy.mock(cls=WellCore) + trash_well = decoy.mock(cls=Well) + trash_well_core = decoy.mock(cls=WellCore) + trash_location = Location(Point(7, 8, 9), labware=trash_well) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + air_gap_volume = ( + sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) + ) + sample_transfer_props.dispense.retract.blowout.location = BlowoutLocation.TRASH + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 1, 1), labware=None), + target_well=dest_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(dest_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(dest_well.get_top(0)).then_return(well_top_point) + decoy.when(trash_well._core).then_return(trash_well_core) + subject.retract_after_dispensing( + trash_location=trash_location, + source_location=source_location, + source_well=source_well, + add_final_air_gap=True, + ) + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=50, + ), + mock_instrument_core.delay(10), + mock_instrument_core.touch_tip( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + radius=1, + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + mock_instrument_core.air_gap_in_place( + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), + mock_instrument_core.set_flow_rate(blow_out=100), + mock_instrument_core.blow_out( + location=trash_location, + well_core=None, + in_place=False, + ), + mock_instrument_core.touch_tip( + location=trash_location, + well_core=trash_well_core, + radius=1, + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=trash_location, + well_core=trash_well_core, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + *( + add_final_air_gap + and [ + mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] + ] + or [] + ), + ) + + +@pytest.mark.parametrize( + "add_final_air_gap", + [True, False], +) +def test_retract_after_dispense_with_blowout_in_disposal_location( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + sample_transfer_props: TransferProperties, + add_final_air_gap: bool, +) -> None: + """It should execute steps to retract from well after a dispense.""" + source_location = Location(Point(1, 2, 3), labware=None) + source_well = decoy.mock(cls=WellCore) + dest_well = decoy.mock(cls=WellCore) + trash_location = decoy.mock(cls=TrashBin) + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + air_gap_volume = ( + sample_transfer_props.aspirate.retract.air_gap_by_volume.get_for_volume(0) + ) + sample_transfer_props.dispense.retract.blowout.location = BlowoutLocation.TRASH + + subject = TransferComponentsExecutor( + instrument_core=mock_instrument_core, + transfer_properties=sample_transfer_props, + target_location=Location(Point(1, 1, 1), labware=None), + target_well=dest_well, + tip_state=TipState(), + transfer_type=TransferType.ONE_TO_ONE, + ) + decoy.when(dest_well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(dest_well.get_top(0)).then_return(well_top_point) + subject.retract_after_dispensing( + trash_location=trash_location, + source_location=source_location, + source_well=source_well, + add_final_air_gap=True, + ) + decoy.verify( + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=50, + ), + mock_instrument_core.delay(10), + mock_instrument_core.touch_tip( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + radius=1, + z_offset=-1, + speed=30, + ), + mock_instrument_core.move_to( + location=Location(Point(12, 24, 36), labware=None), + well_core=dest_well, + force_direct=True, + minimum_z_height=None, + speed=None, + ), + mock_instrument_core.air_gap_in_place( + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), + mock_instrument_core.set_flow_rate(blow_out=100), + mock_instrument_core.blow_out( + location=trash_location, + well_core=None, + in_place=False, + ), + *( + add_final_air_gap + and [ + mock_instrument_core.air_gap_in_place( # type: ignore[func-returns-value] + volume=air_gap_volume, flow_rate=air_gap_volume + ), + mock_instrument_core.delay(0.2), # type: ignore[func-returns-value] + ] + or [] + ), + ) + + +@pytest.mark.parametrize( + argnames=["position_reference", "offset", "expected_result"], + argvalues=[ + (PositionReference.WELL_TOP, Coordinate(x=11, y=12, z=13), Point(12, 14, 16)), + ( + PositionReference.WELL_BOTTOM, + Coordinate(x=21, y=22, z=23), + Point(25, 27, 29), + ), + ( + PositionReference.WELL_CENTER, + Coordinate(x=31, y=32, z=33), + Point(38, 40, 42), + ), + ], +) +def test_absolute_point_from_position_reference_and_offset( + decoy: Decoy, + position_reference: PositionReference, + offset: Coordinate, + expected_result: Point, +) -> None: + """It should return the correct absolute point based on well, position reference and offset.""" + well = decoy.mock(cls=WellCore) + + well_top_point = Point(1, 2, 3) + well_bottom_point = Point(4, 5, 6) + well_center_point = Point(7, 8, 9) + decoy.when(well.get_bottom(0)).then_return(well_bottom_point) + decoy.when(well.get_top(0)).then_return(well_top_point) + decoy.when(well.get_center()).then_return(well_center_point) + + assert ( + absolute_point_from_position_reference_and_offset( + well=well, position_reference=position_reference, offset=offset + ) + == expected_result + ) + + +def test_absolute_point_from_position_reference_and_offset_raises_errors( + decoy: Decoy, +) -> None: + """It should raise errors for invalid input.""" + well = decoy.mock(cls=WellCore) + with pytest.raises(NotImplementedError): + absolute_point_from_position_reference_and_offset( + well=well, + position_reference=PositionReference.LIQUID_MENISCUS, + offset=Coordinate(x=0, y=0, z=0), + ) + + with pytest.raises(ValueError, match="Unknown position reference"): + absolute_point_from_position_reference_and_offset( + well=well, + position_reference="PositionReference", # type: ignore[arg-type] + offset=Coordinate(x=0, y=0, z=0), + ) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 3f639aff922..c73a154c937 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1950,7 +1950,7 @@ def test_transfer_liquid_delegates_to_engine_core( trash_location = Location(point=Point(1, 2, 3), labware=mock_well) next_tiprack = decoy.mock(cls=Labware) subject.starting_tip = None - subject.tip_racks = tip_racks + subject._tip_racks = tip_racks decoy.when(mock_protocol_core.robot_type).then_return(robot_type) decoy.when( @@ -1964,43 +1964,28 @@ def test_transfer_liquid_delegates_to_engine_core( ) decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when(mock_instrument_core.get_active_channels()).then_return(2) - decoy.when( - labware.next_available_tip( - starting_tip=None, - tip_racks=tip_racks, - channels=2, - nozzle_map=MOCK_MAP, - ) - ).then_return((next_tiprack, decoy.mock(cls=Well))) decoy.when(mock_instrument_core.get_current_volume()).then_return(0) decoy.when( - mock_validation.ensure_valid_tip_drop_location_for_transfer_v2(trash_location) + mock_validation.ensure_valid_trash_location_for_transfer_v2(trash_location) ).then_return(trash_location.move(Point(1, 2, 3))) decoy.when(next_tiprack.uri).then_return("tiprack-uri") decoy.when(mock_instrument_core.get_pipette_name()).then_return("pipette-name") - decoy.when( - mock_instrument_core.load_liquid_class( - liquid_class=test_liq_class, - pipette_load_name="pipette-name", - tiprack_uri="tiprack-uri", - ) - ).then_return("liq-class-id") - subject.transfer_liquid( liquid_class=test_liq_class, volume=10, source=[mock_well], dest=[mock_well], new_tip="never", - tip_drop_location=trash_location, + trash_location=trash_location, ) decoy.verify( mock_instrument_core.transfer_liquid( - liquid_class_id="liq-class-id", + liquid_class=test_liq_class, volume=10, - source=[mock_well._core], - dest=[mock_well._core], + source=[(Location(Point(), labware=mock_well), mock_well._core)], + dest=[(Location(Point(), labware=mock_well), mock_well._core)], new_tip=TransferTipPolicyV2.ONCE, + tip_racks=[(Location(Point(), labware=tip_racks[0]), tip_racks[0]._core)], trash_location=trash_location.move(Point(1, 2, 3)), ) ) diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py index 94e6dd49205..ee269f378d5 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -58,6 +58,62 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.as_shared_data_model() == aspirate_data +def test_aspirate_settings_overrides() -> None: + """It should allow aspirate properties to be overridden with new values.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate + + aspirate_properties = build_aspirate_properties(aspirate_data) + + aspirate_properties.submerge.position_reference = "well-bottom" # type: ignore[assignment] + assert aspirate_properties.submerge.position_reference.value == "well-bottom" + aspirate_properties.submerge.offset = 5, 0, 0 # type: ignore[assignment] + assert aspirate_properties.submerge.offset == Coordinate(x=5, y=0, z=0) + aspirate_properties.submerge.speed = 123 + assert aspirate_properties.submerge.speed == 123 + aspirate_properties.submerge.delay.enabled = False + assert aspirate_properties.submerge.delay.enabled is False + aspirate_properties.submerge.delay.duration = 5.1 + assert aspirate_properties.submerge.delay.duration == 5.1 + + aspirate_properties.retract.position_reference = "well-center" # type: ignore[assignment] + assert aspirate_properties.retract.position_reference.value == "well-center" + aspirate_properties.retract.offset = 0, 50, 0 # type: ignore[assignment] + assert aspirate_properties.retract.offset == Coordinate(x=0, y=50, z=0) + aspirate_properties.retract.speed = 987 + assert aspirate_properties.retract.speed == 987 + aspirate_properties.retract.touch_tip.enabled = False + assert aspirate_properties.retract.touch_tip.enabled is False + aspirate_properties.retract.touch_tip.z_offset = 2.34 + assert aspirate_properties.retract.touch_tip.z_offset == 2.34 + aspirate_properties.retract.touch_tip.mm_to_edge = 4.56 + assert aspirate_properties.retract.touch_tip.mm_to_edge == 4.56 + aspirate_properties.retract.touch_tip.speed = 501 + assert aspirate_properties.retract.touch_tip.speed == 501 + aspirate_properties.retract.delay.enabled = False + assert aspirate_properties.retract.delay.enabled is False + aspirate_properties.retract.delay.duration = 0.5 + assert aspirate_properties.retract.delay.duration == 0.5 + + aspirate_properties.position_reference = "liquid-meniscus" # type: ignore[assignment] + assert aspirate_properties.position_reference.value == "liquid-meniscus" + aspirate_properties.offset = -1, -2, -3 # type: ignore[assignment] + assert aspirate_properties.offset == Coordinate(x=-1, y=-2, z=-3) + aspirate_properties.pre_wet = False + assert aspirate_properties.pre_wet is False + aspirate_properties.mix.enabled = False + assert aspirate_properties.mix.enabled is False + aspirate_properties.mix.repetitions = 33 + assert aspirate_properties.mix.repetitions == 33 + aspirate_properties.mix.volume = 51 + assert aspirate_properties.mix.volume == 51 + aspirate_properties.delay.enabled = False + assert aspirate_properties.delay.enabled is False + aspirate_properties.delay.duration = 2.3 + assert aspirate_properties.delay.duration == 2.3 + + def test_build_single_dispense_settings() -> None: """It should convert the shared data single dispense settings to the PAPI type.""" fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") @@ -115,6 +171,67 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.as_shared_data_model() == single_dispense_data +def test_single_dispense_settings_override() -> None: + """It should allow single dispense properties to be overridden with new values.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense + + single_dispense_properties = build_single_dispense_properties(single_dispense_data) + + single_dispense_properties.submerge.position_reference = "well-bottom" # type: ignore[assignment] + assert single_dispense_properties.submerge.position_reference.value == "well-bottom" + single_dispense_properties.submerge.offset = 3, -2, 1 # type: ignore[assignment] + assert single_dispense_properties.submerge.offset == Coordinate(x=3, y=-2, z=1) + single_dispense_properties.submerge.speed = 111 + assert single_dispense_properties.submerge.speed == 111 + single_dispense_properties.submerge.delay.enabled = False + assert single_dispense_properties.submerge.delay.enabled is False + single_dispense_properties.submerge.delay.duration = 5.1 + assert single_dispense_properties.submerge.delay.duration == 5.1 + + single_dispense_properties.retract.position_reference = "well-center" # type: ignore[assignment] + assert single_dispense_properties.retract.position_reference.value == "well-center" + single_dispense_properties.retract.offset = -9, -8, -7 # type: ignore[assignment] + assert single_dispense_properties.retract.offset == Coordinate(x=-9, y=-8, z=-7) + single_dispense_properties.retract.speed = 222 + assert single_dispense_properties.retract.speed == 222 + single_dispense_properties.retract.touch_tip.enabled = False + assert single_dispense_properties.retract.touch_tip.enabled is False + single_dispense_properties.retract.touch_tip.z_offset = 2.34 + assert single_dispense_properties.retract.touch_tip.z_offset == 2.34 + single_dispense_properties.retract.touch_tip.mm_to_edge = 1.11 + assert single_dispense_properties.retract.touch_tip.mm_to_edge == 1.11 + single_dispense_properties.retract.touch_tip.speed = 543 + assert single_dispense_properties.retract.touch_tip.speed == 543 + single_dispense_properties.retract.blowout.enabled = False + assert single_dispense_properties.retract.blowout.enabled is False + single_dispense_properties.retract.blowout.location = "destination" # type: ignore[assignment] + assert single_dispense_properties.retract.blowout.location + assert single_dispense_properties.retract.blowout.location.value == "destination" + single_dispense_properties.retract.blowout.flow_rate = 3.21 + assert single_dispense_properties.retract.blowout.flow_rate == 3.21 + single_dispense_properties.retract.delay.enabled = False + assert single_dispense_properties.retract.delay.enabled is False + single_dispense_properties.retract.delay.duration = 0.1 + assert single_dispense_properties.retract.delay.duration == 0.1 + + single_dispense_properties.position_reference = "liquid-meniscus" # type: ignore[assignment] + assert single_dispense_properties.position_reference.value == "liquid-meniscus" + single_dispense_properties.offset = 11, 22, -33 # type: ignore[assignment] + assert single_dispense_properties.offset == Coordinate(x=11, y=22, z=-33) + single_dispense_properties.mix.enabled = False + assert single_dispense_properties.mix.enabled is False + single_dispense_properties.mix.repetitions = 15 + assert single_dispense_properties.mix.repetitions == 15 + single_dispense_properties.mix.volume = 3 + assert single_dispense_properties.mix.volume == 3 + single_dispense_properties.delay.enabled = False + assert single_dispense_properties.delay.enabled is False + single_dispense_properties.delay.duration = 25.25 + assert single_dispense_properties.delay.duration == 25.25 + + def test_build_multi_dispense_settings() -> None: """It should convert the shared data multi dispense settings to the PAPI type.""" fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") @@ -171,6 +288,62 @@ def test_build_multi_dispense_settings() -> None: assert multi_dispense_properties.as_shared_data_model() == multi_dispense_data +def test_multi_dispense_settings_override() -> None: + """It should allow multi dispense properties to be overridden with new values.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense + assert multi_dispense_data is not None + multi_dispense_properties = build_multi_dispense_properties(multi_dispense_data) + assert multi_dispense_properties is not None + + multi_dispense_properties.submerge.position_reference = "well-bottom" # type: ignore[assignment] + assert multi_dispense_properties.submerge.position_reference.value == "well-bottom" + multi_dispense_properties.submerge.offset = 3, -2, 1 # type: ignore[assignment] + assert multi_dispense_properties.submerge.offset == Coordinate(x=3, y=-2, z=1) + multi_dispense_properties.submerge.speed = 111 + assert multi_dispense_properties.submerge.speed == 111 + multi_dispense_properties.submerge.delay.enabled = False + assert multi_dispense_properties.submerge.delay.enabled is False + multi_dispense_properties.submerge.delay.duration = 5.1 + assert multi_dispense_properties.submerge.delay.duration == 5.1 + + multi_dispense_properties.retract.position_reference = "well-center" # type: ignore[assignment] + assert multi_dispense_properties.retract.position_reference.value == "well-center" + multi_dispense_properties.retract.offset = -9, -8, -7 # type: ignore[assignment] + assert multi_dispense_properties.retract.offset == Coordinate(x=-9, y=-8, z=-7) + multi_dispense_properties.retract.speed = 222 + assert multi_dispense_properties.retract.speed == 222 + multi_dispense_properties.retract.touch_tip.enabled = False + assert multi_dispense_properties.retract.touch_tip.enabled is False + multi_dispense_properties.retract.touch_tip.z_offset = 2.34 + assert multi_dispense_properties.retract.touch_tip.z_offset == 2.34 + multi_dispense_properties.retract.touch_tip.mm_to_edge = 1.11 + assert multi_dispense_properties.retract.touch_tip.mm_to_edge == 1.11 + multi_dispense_properties.retract.touch_tip.speed = 543 + assert multi_dispense_properties.retract.touch_tip.speed == 543 + multi_dispense_properties.retract.blowout.enabled = False + assert multi_dispense_properties.retract.blowout.enabled is False + multi_dispense_properties.retract.blowout.location = "destination" # type: ignore[assignment] + assert multi_dispense_properties.retract.blowout.location + assert multi_dispense_properties.retract.blowout.location.value == "destination" + multi_dispense_properties.retract.blowout.flow_rate = 3.21 + assert multi_dispense_properties.retract.blowout.flow_rate == 3.21 + multi_dispense_properties.retract.delay.enabled = False + assert multi_dispense_properties.retract.delay.enabled is False + multi_dispense_properties.retract.delay.duration = 0.1 + assert multi_dispense_properties.retract.delay.duration == 0.1 + + multi_dispense_properties.position_reference = "liquid-meniscus" # type: ignore[assignment] + assert multi_dispense_properties.position_reference.value == "liquid-meniscus" + multi_dispense_properties.offset = 11, 22, -33 # type: ignore[assignment] + assert multi_dispense_properties.offset == Coordinate(x=11, y=22, z=-33) + multi_dispense_properties.delay.enabled = False + assert multi_dispense_properties.delay.enabled is False + multi_dispense_properties.delay.duration = 25.25 + assert multi_dispense_properties.delay.duration == 25.25 + + def test_build_multi_dispense_settings_none( minimal_liquid_class_def2: LiquidClassSchemaV1, ) -> None: @@ -204,3 +377,15 @@ def test_liquid_handling_property_by_volume() -> None: # Test bounds assert subject.get_for_volume(1) == 50.0 assert subject.get_for_volume(1000) == 250.0 + + +def test_non_existent_property_raises_error() -> None: + """It should raise an attribute error if the set property does not exist.""" + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data) + aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate + + aspirate_properties = build_aspirate_properties(aspirate_data) + + with pytest.raises(AttributeError): + aspirate_properties.mix.enable = True # type: ignore[attr-defined] diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index e804ac9dd11..80728b7820c 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -742,6 +742,115 @@ def test_load_labware_on_adapter( decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) +@pytest.mark.parametrize("api_version", [APIVersion(2, 23)]) +def test_load_labware_with_lid( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should create a labware with a lid on it using its execution core.""" + mock_labware_core = decoy.mock(cls=LabwareCore) + mock_lid_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( + "lowercase_labware" + ) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID")).then_return( + "lowercase_lid" + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_C1) + + decoy.when( + mock_core.load_labware( + load_name="lowercase_labware", + location=DeckSlotName.SLOT_C1, + label="some_display_name", + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_labware_core) + decoy.when(mock_lid_core.get_well_columns()).then_return([]) + + decoy.when( + mock_core.load_lid( + load_name="lowercase_lid", + location=mock_labware_core, + namespace="some_namespace", + version=None, + ) + ).then_return(mock_lid_core) + + decoy.when(mock_labware_core.get_name()).then_return("Full Name") + decoy.when(mock_labware_core.get_display_name()).then_return("Display Name") + decoy.when(mock_labware_core.get_well_columns()).then_return([]) + + result = subject.load_labware( + load_name="UPPERCASE_LABWARE", + location=42, + label="some_display_name", + namespace="some_namespace", + version=1337, + lid="UPPERCASE_LID", + ) + + assert isinstance(result, Labware) + assert result.name == "Full Name" + + decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) + + +@pytest.mark.parametrize("api_version", [APIVersion(2, 23)]) +def test_load_lid_stack( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should create a labware with a lid on it using its execution core.""" + mock_lid_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LID")).then_return( + "lowercase_lid" + ) + + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(DeckSlotName.SLOT_C1) + + decoy.when( + mock_core.load_lid_stack( + load_name="lowercase_lid", + location=DeckSlotName.SLOT_C1, + quantity=5, + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_lid_core) + + decoy.when(mock_lid_core.get_name()).then_return("STACK_OBJECT") + decoy.when(mock_lid_core.get_display_name()).then_return("") + decoy.when(mock_lid_core.get_well_columns()).then_return([]) + + result = subject.load_lid_stack( + load_name="UPPERCASE_LID", + location=42, + quantity=5, + namespace="some_namespace", + version=1337, + ) + + assert isinstance(result, Labware) + assert result.name == "STACK_OBJECT" + + def test_loaded_labware( decoy: Decoy, mock_core_map: LoadedCoreMap, diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index ce12d1a8f53..1fb29fec6cc 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -9,6 +9,7 @@ from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, LabwareRole, Parameters as LabwareDefinitionParameters, ) @@ -33,7 +34,6 @@ HeaterShakerModuleModel, ThermocyclerStep, ) -from opentrons.protocols.models import LabwareDefinition from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocol_api import ( @@ -834,43 +834,48 @@ def test_ensure_valid_flat_wells_list(decoy: Decoy) -> None: ) == [target1, target1, target2, target2] -def test_ensure_valid_tip_drop_location_for_transfer_v2( +def test_ensure_valid_trash_location_for_transfer_v2( decoy: Decoy, ) -> None: - """It should check that the tip drop location is valid.""" + """It should check that the trash location is valid.""" mock_well = decoy.mock(cls=Well) mock_location = Location(point=Point(x=1, y=1, z=1), labware=mock_well) mock_trash_bin = decoy.mock(cls=TrashBin) mock_waste_chute = decoy.mock(cls=WasteChute) - assert ( - subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_well) == mock_well + decoy.when(mock_well.top()).then_return(Location(Point(1, 2, 3), labware=mock_well)) + assert subject.ensure_valid_trash_location_for_transfer_v2(mock_well) == Location( + Point(1, 2, 3), labware=mock_well ) assert ( - subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_location) + subject.ensure_valid_trash_location_for_transfer_v2(mock_location) == mock_location ) assert ( - subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_trash_bin) + subject.ensure_valid_trash_location_for_transfer_v2(mock_trash_bin) == mock_trash_bin ) assert ( - subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_waste_chute) + subject.ensure_valid_trash_location_for_transfer_v2(mock_waste_chute) == mock_waste_chute ) -def test_ensure_valid_tip_drop_location_for_transfer_v2_raises(decoy: Decoy) -> None: - """It should raise an error for invalid tip drop locations.""" +def test_ensure_valid_trash_location_for_transfer_v2_raises(decoy: Decoy) -> None: + """It should raise an error for invalid trash locations.""" with pytest.raises(TypeError, match="However, it is '\\['a'\\]'"): - subject.ensure_valid_tip_drop_location_for_transfer_v2(["a"]) # type: ignore[arg-type] + subject.ensure_valid_trash_location_for_transfer_v2( + ["a"] # type: ignore[arg-type] + ) mock_labware = decoy.mock(cls=Labware) with pytest.raises(TypeError, match=f"However, it is '{mock_labware}'"): - subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_labware) # type: ignore[arg-type] + subject.ensure_valid_trash_location_for_transfer_v2( + mock_labware # type: ignore[arg-type] + ) with pytest.raises( TypeError, match="However, the given location doesn't refer to any well." ): - subject.ensure_valid_tip_drop_location_for_transfer_v2( + subject.ensure_valid_trash_location_for_transfer_v2( Location(point=Point(x=1, y=1, z=1), labware=None) ) diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 20bbd2b646c..54112941c4c 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -31,20 +31,17 @@ def test_liquid_class_creation_and_property_fetching( # TODO (spp, 2024-10-17): update this to fetch pipette load name from instrument context assert ( water.get_for( - pipette_load_name, tiprack.load_name + pipette_load_name, tiprack.uri ).dispense.flow_rate_by_volume.get_for_volume(1) == 50 ) - assert ( - water.get_for(pipette_load_name, tiprack.load_name).aspirate.submerge.speed - == 100 - ) + assert water.get_for(pipette_load_name, tiprack.uri).aspirate.submerge.speed == 100 with pytest.raises( ValueError, match="No properties found for non-existent-pipette in water liquid class", ): - water.get_for("non-existent-pipette", tiprack.load_name) + water.get_for("non-existent-pipette", tiprack.uri) with pytest.raises(AttributeError): water.name = "foo" # type: ignore diff --git a/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py new file mode 100644 index 00000000000..24a5efc0dab --- /dev/null +++ b/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py @@ -0,0 +1,363 @@ +"""Tests for the transfer APIs using liquid classes.""" +import pytest +import mock +from decoy import Decoy +from opentrons_shared_data.robot.types import RobotTypeEnum + +from opentrons.protocol_api import ProtocolContext +from opentrons.config import feature_flags as ff +from opentrons.protocol_api.core.engine import InstrumentCore +from opentrons.protocol_api.core.engine.transfer_components_executor import ( + TransferType, + LiquidAndAirGapPair, +) + + +@pytest.mark.ot3_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_water_transfer_with_volume_more_than_tip_max( + decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext +) -> None: + """It should run the transfer steps without any errors. + + This test only checks that various supported configurations for a transfer + analyze successfully. It doesn't check whether the steps are as expected. + That will be covered in analysis snapshot tests. + """ + decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) + trash = simulated_protocol_context.load_trash_bin("A3") + tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D1" + ) + pipette_50 = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left", tip_racks=[tiprack] + ) + nest_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "C3" + ) + arma_plate = simulated_protocol_context.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" + ) + + water = simulated_protocol_context.define_liquid_class("water") + with mock.patch.object( + InstrumentCore, + "pick_up_tip", + side_effect=InstrumentCore.pick_up_tip, + autospec=True, + ) as patched_pick_up_tip: + mock_manager = mock.Mock() + mock_manager.attach_mock(patched_pick_up_tip, "pick_up_tip") + + pipette_50.transfer_liquid( + liquid_class=water, + volume=60, + source=nest_plate.rows()[0], + dest=arma_plate.rows()[0], + new_tip="always", + trash_location=trash, + ) + assert patched_pick_up_tip.call_count == 24 + patched_pick_up_tip.reset_mock() + + pipette_50.transfer_liquid( + liquid_class=water, + volume=100, + source=nest_plate.rows()[0], + dest=arma_plate.rows()[0], + new_tip="per source", + trash_location=trash, + ) + assert patched_pick_up_tip.call_count == 12 + patched_pick_up_tip.reset_mock() + + pipette_50.pick_up_tip() + pipette_50.transfer_liquid( + liquid_class=water, + volume=50, + source=nest_plate.rows()[0], + dest=arma_plate.rows()[0], + new_tip="never", + trash_location=trash, + ) + pipette_50.drop_tip() + assert patched_pick_up_tip.call_count == 1 + + +@pytest.mark.ot3_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_order_of_water_transfer_steps( + decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext +) -> None: + """It should run the transfer steps without any errors. + + This test only checks that various supported configurations for a transfer + analyze successfully. It doesn't check whether the steps are as expected. + That will be covered in analysis snapshot tests. + """ + decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) + trash = simulated_protocol_context.load_trash_bin("A3") + tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D1" + ) + pipette_50 = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left", tip_racks=[tiprack] + ) + nest_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "C3" + ) + arma_plate = simulated_protocol_context.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" + ) + + water = simulated_protocol_context.define_liquid_class("water") + with ( + mock.patch.object( + InstrumentCore, + "load_liquid_class", + side_effect=InstrumentCore.load_liquid_class, + autospec=True, + ) as patched_load_liquid_class, + mock.patch.object( + InstrumentCore, + "pick_up_tip", + side_effect=InstrumentCore.pick_up_tip, + autospec=True, + ) as patched_pick_up_tip, + mock.patch.object( + InstrumentCore, + "aspirate_liquid_class", + side_effect=InstrumentCore.aspirate_liquid_class, + autospec=True, + ) as patched_aspirate, + mock.patch.object( + InstrumentCore, + "dispense_liquid_class", + side_effect=InstrumentCore.dispense_liquid_class, + autospec=True, + ) as patched_dispense, + mock.patch.object( + InstrumentCore, + "drop_tip_in_disposal_location", + side_effect=InstrumentCore.drop_tip_in_disposal_location, + autospec=True, + ) as patched_drop_tip, + ): + mock_manager = mock.Mock() + mock_manager.attach_mock(patched_pick_up_tip, "pick_up_tip") + mock_manager.attach_mock(patched_load_liquid_class, "load_liquid_class") + mock_manager.attach_mock(patched_aspirate, "aspirate_liquid_class") + mock_manager.attach_mock(patched_dispense, "dispense_liquid_class") + mock_manager.attach_mock(patched_drop_tip, "drop_tip_in_disposal_location") + pipette_50.transfer_liquid( + liquid_class=water, + volume=40, + source=nest_plate.rows()[0][:2], + dest=arma_plate.rows()[0][:2], + new_tip="always", + trash_location=trash, + ) + expected_calls = [ + mock.call.load_liquid_class( + mock.ANY, + name="water", + transfer_properties=mock.ANY, + tiprack_uri="opentrons/opentrons_flex_96_tiprack_50ul/1", + ), + mock.call.pick_up_tip( + mock.ANY, + location=mock.ANY, + well_core=mock.ANY, + presses=mock.ANY, + increment=mock.ANY, + ), + mock.call.aspirate_liquid_class( + mock.ANY, + volume=40, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0)], + ), + mock.call.dispense_liquid_class( + mock.ANY, + volume=40, + dest=mock.ANY, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=40, air_gap=0.1)], + add_final_air_gap=True, + trash_location=mock.ANY, + ), + mock.call.drop_tip_in_disposal_location( + mock.ANY, + disposal_location=trash, + home_after=False, + alternate_tip_drop=True, + ), + mock.call.pick_up_tip( + mock.ANY, + location=mock.ANY, + well_core=mock.ANY, + presses=mock.ANY, + increment=mock.ANY, + ), + mock.call.aspirate_liquid_class( + mock.ANY, + volume=40, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0)], + ), + mock.call.dispense_liquid_class( + mock.ANY, + volume=40, + dest=mock.ANY, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=40, air_gap=0.1)], + add_final_air_gap=True, + trash_location=mock.ANY, + ), + mock.call.drop_tip_in_disposal_location( + mock.ANY, + disposal_location=trash, + home_after=False, + alternate_tip_drop=True, + ), + ] + assert len(mock_manager.mock_calls) == 9 + assert mock_manager.mock_calls == expected_calls + + +@pytest.mark.ot3_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_order_of_water_transfer_steps_with_no_new_tips( + decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext +) -> None: + """It should run the transfer steps without any errors. + + This test only checks that various supported configurations for a transfer + analyze successfully. It doesn't check whether the steps are as expected. + That will be covered in analysis snapshot tests. + """ + decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) + trash = simulated_protocol_context.load_trash_bin("A3") + tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D1" + ) + pipette_50 = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left", tip_racks=[tiprack] + ) + nest_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "C3" + ) + arma_plate = simulated_protocol_context.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" + ) + + water = simulated_protocol_context.define_liquid_class("water") + pipette_50.pick_up_tip() + with ( + mock.patch.object( + InstrumentCore, + "load_liquid_class", + side_effect=InstrumentCore.load_liquid_class, + autospec=True, + ) as patched_load_liquid_class, + mock.patch.object( + InstrumentCore, + "pick_up_tip", + side_effect=InstrumentCore.pick_up_tip, + autospec=True, + ) as patched_pick_up_tip, + mock.patch.object( + InstrumentCore, + "aspirate_liquid_class", + side_effect=InstrumentCore.aspirate_liquid_class, + autospec=True, + ) as patched_aspirate, + mock.patch.object( + InstrumentCore, + "dispense_liquid_class", + side_effect=InstrumentCore.dispense_liquid_class, + autospec=True, + ) as patched_dispense, + mock.patch.object( + InstrumentCore, + "drop_tip_in_disposal_location", + side_effect=InstrumentCore.drop_tip_in_disposal_location, + autospec=True, + ) as patched_drop_tip, + ): + mock_manager = mock.Mock() + mock_manager.attach_mock(patched_pick_up_tip, "pick_up_tip") + mock_manager.attach_mock(patched_load_liquid_class, "load_liquid_class") + mock_manager.attach_mock(patched_aspirate, "aspirate_liquid_class") + mock_manager.attach_mock(patched_dispense, "dispense_liquid_class") + mock_manager.attach_mock(patched_drop_tip, "drop_tip_in_disposal_location") + pipette_50.transfer_liquid( + liquid_class=water, + volume=40, + source=nest_plate.rows()[0][:2], + dest=arma_plate.rows()[0][:2], + new_tip="never", + trash_location=trash, + ) + expected_calls = [ + mock.call.load_liquid_class( + mock.ANY, + name="water", + transfer_properties=mock.ANY, + tiprack_uri="opentrons/opentrons_flex_96_tiprack_50ul/1", + ), + mock.call.aspirate_liquid_class( + mock.ANY, + volume=40, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0)], + ), + mock.call.dispense_liquid_class( + mock.ANY, + volume=40, + dest=mock.ANY, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=40, air_gap=0.1)], + add_final_air_gap=True, + trash_location=mock.ANY, + ), + mock.call.aspirate_liquid_class( + mock.ANY, + volume=40, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0.1)], + ), + mock.call.dispense_liquid_class( + mock.ANY, + volume=40, + dest=mock.ANY, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=40, air_gap=0.1)], + add_final_air_gap=False, + trash_location=mock.ANY, + ), + ] + assert len(mock_manager.mock_calls) == len(expected_calls) + assert mock_manager.mock_calls[2] == expected_calls[2] diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/__init__.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/__init__.py new file mode 100644 index 00000000000..ebb91ede166 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/__init__.py @@ -0,0 +1 @@ +"""Tests for absorbance reader commands.""" diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py new file mode 100644 index 00000000000..c40a825d9c6 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py @@ -0,0 +1,221 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy + +from opentrons.drivers.types import ( + AbsorbanceReaderLidStatus, +) +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine import ModuleModel, DeckSlotLocation +from opentrons.protocol_engine.errors import CannotPerformModuleAction + +from opentrons.protocol_engine.execution import EquipmentHandler, LabwareMovementHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + CloseLidResult, + CloseLidParams, +) +from opentrons.protocol_engine.commands.absorbance_reader.close_lid import ( + CloseLidImpl, +) +from opentrons.protocol_engine.types import ( + LabwareMovementOffsetData, + LabwareOffsetVector, +) +from opentrons.types import DeckSlotName +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + Parameters, +) + + +@pytest.fixture +def absorbance_def() -> LabwareDefinition: + """Get a tip rack Pydantic model definition value object.""" + return LabwareDefinition.model_construct( # type: ignore[call-arg] + namespace="test", + version=1, + parameters=Parameters.model_construct( # type: ignore[call-arg] + loadName="cool-labware", + tipOverlap=None, # add a None value to validate serialization to dictionary + ), + ) + + +@pytest.fixture +def subject( + state_view: StateView, + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, +) -> CloseLidImpl: + """Subject fixture.""" + return CloseLidImpl( + state_view=state_view, equipment=equipment, labware_movement=labware_movement + ) + + +@pytest.mark.parametrize( + "hardware_lid_status", + (AbsorbanceReaderLidStatus.ON, AbsorbanceReaderLidStatus.OFF), +) +async def test_absorbance_reader_close_lid_implementation( + decoy: Decoy, + subject: CloseLidImpl, + state_view: StateView, + equipment: EquipmentHandler, + hardware_lid_status: AbsorbanceReaderLidStatus, + absorbance_def: LabwareDefinition, +) -> None: + """It should validate, find hardware module if not virtualized, and close lid.""" + params = CloseLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + hardware_lid_status + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), + ) + ) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=CloseLidResult(), + state_update=update_types.StateUpdate( + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( + is_lid_on=True + ), + ), + ), + ) + + +async def test_close_lid_raises_no_module( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: CloseLidImpl, +) -> None: + """Should raise an error that the hardware module not found.""" + params = CloseLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + decoy.when(state_view.config.use_virtual_modules).then_return(False) + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return(None) + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + +async def test_close_lid_raises_no_gripper_offset( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: CloseLidImpl, + absorbance_def: LabwareDefinition, +) -> None: + """Should raise an error that gripper offset not found.""" + params = CloseLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + AbsorbanceReaderLidStatus.OFF + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return(None) + with pytest.raises(ValueError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_initialize.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_initialize.py new file mode 100644 index 00000000000..efdd8908583 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_initialize.py @@ -0,0 +1,238 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy +from typing import List + +from opentrons.drivers.types import ABSMeasurementMode +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine.errors import InvalidWavelengthError + +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + InitializeParams, + InitializeResult, +) +from opentrons.protocol_engine.commands.absorbance_reader.initialize import ( + InitializeImpl, +) + + +@pytest.fixture +def subject( + state_view: StateView, + equipment: EquipmentHandler, +) -> InitializeImpl: + """Subject command implementation to test.""" + return InitializeImpl(state_view=state_view, equipment=equipment) + + +@pytest.mark.parametrize( + "input_sample_wave_length, input_measure_mode", [([1, 2], "multi"), ([1], "single")] +) +async def test_absorbance_reader_implementation( + decoy: Decoy, + input_sample_wave_length: List[int], + input_measure_mode: str, + subject: InitializeImpl, + state_view: StateView, + equipment: EquipmentHandler, +) -> None: + """It should validate, find hardware module if not virtualized, and disengage.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode=input_measure_mode, # type: ignore[arg-type] + sampleWavelengths=input_sample_wave_length, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + result = await subject.execute(params=params) + + decoy.verify( + await absorbance_module_hw.set_sample_wavelength( + ABSMeasurementMode(params.measureMode), + params.sampleWavelengths, + reference_wavelength=params.referenceWavelength, + ), + times=1, + ) + assert result == SuccessData( + public=InitializeResult(), + state_update=update_types.StateUpdate( + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + initialize_absorbance_reader_update=update_types.AbsorbanceReaderInitializeUpdate( + measure_mode=input_measure_mode, # type: ignore[arg-type] + sample_wave_lengths=input_sample_wave_length, + reference_wave_length=None, + ), + ) + ), + ) + + +@pytest.mark.parametrize( + "input_sample_wave_length, input_measure_mode", + [([1, 2, 3], "multi"), ([3], "single")], +) +async def test_initialize_raises_invalid_wave_length( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, + input_sample_wave_length: List[int], + input_measure_mode: str, +) -> None: + """Should raise an InvalidWavelengthError error.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode=input_measure_mode, # type: ignore[arg-type] + sampleWavelengths=input_sample_wave_length, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(InvalidWavelengthError): + await subject.execute(params=params) + + +@pytest.mark.parametrize( + "input_sample_wave_length, input_measure_mode", + [([], "multi"), ([], "single")], +) +async def test_initialize_raises_measure_mode_not_matching( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, + input_sample_wave_length: List[int], + input_measure_mode: str, +) -> None: + """Should raise an error that the measure mode does not match sample wave.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode=input_measure_mode, # type: ignore[arg-type] + sampleWavelengths=input_sample_wave_length, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(ValueError): + await subject.execute(params=params) + + +async def test_initialize_single_raises_reference_wave_length_not_matching( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, +) -> None: + """Should raise an error that the measure mode does not match sample wave.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode="single", + sampleWavelengths=[1], + referenceWavelength=3, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(InvalidWavelengthError): + await subject.execute(params=params) + + +async def test_initialize_multi_raises_no_reference_wave_length( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: InitializeImpl, +) -> None: + """Should raise an error that the measure mode does not match sample wave.""" + params = InitializeParams( + moduleId="unverified-module-id", + measureMode="multi", + sampleWavelengths=[1, 2], + referenceWavelength=3, + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when((absorbance_module_hw.supported_wavelengths)).then_return([1, 2]) + + with pytest.raises(ValueError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py new file mode 100644 index 00000000000..bc555a9bb18 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py @@ -0,0 +1,222 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy + +from opentrons.drivers.types import ( + AbsorbanceReaderLidStatus, +) +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine import ModuleModel, DeckSlotLocation +from opentrons.protocol_engine.errors import CannotPerformModuleAction + +from opentrons.protocol_engine.execution import EquipmentHandler, LabwareMovementHandler +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + OpenLidResult, + OpenLidParams, +) +from opentrons.protocol_engine.commands.absorbance_reader.open_lid import ( + OpenLidImpl, +) +from opentrons.protocol_engine.types import ( + LabwareMovementOffsetData, + LabwareOffsetVector, +) +from opentrons.types import DeckSlotName +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + Parameters, +) + + +@pytest.fixture +def absorbance_def() -> LabwareDefinition: + """Get a tip rack Pydantic model definition value object.""" + return LabwareDefinition.model_construct( # type: ignore[call-arg] + namespace="test", + version=1, + parameters=Parameters.model_construct( # type: ignore[call-arg] + loadName="cool-labware", + tipOverlap=None, # add a None value to validate serialization to dictionary + ), + ) + + +@pytest.fixture +def subject( + state_view: StateView, + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, +) -> OpenLidImpl: + """Command implementation subject for testing.""" + return OpenLidImpl( + state_view=state_view, equipment=equipment, labware_movement=labware_movement + ) + + +@pytest.mark.parametrize( + "hardware_lid_status", + (AbsorbanceReaderLidStatus.ON, AbsorbanceReaderLidStatus.OFF), +) +async def test_absorbance_reader_implementation( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, + hardware_lid_status: AbsorbanceReaderLidStatus, + absorbance_def: LabwareDefinition, + subject: OpenLidImpl, +) -> None: + """It should validate, find hardware module if not virtualized, and disengage.""" + params = OpenLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + hardware_lid_status + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return( + LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), + ) + ) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=OpenLidResult(), + state_update=update_types.StateUpdate( + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( + is_lid_on=False + ), + ), + ), + ) + + +async def test_open_lid_raises_no_module( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: OpenLidImpl, +) -> None: + """Should raise an error that the hardware module not found.""" + params = OpenLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + decoy.when(state_view.config.use_virtual_modules).then_return(False) + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return(None) + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + +async def test_open_lid_raises_no_gripper_offset( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + subject: OpenLidImpl, + absorbance_def: LabwareDefinition, +) -> None: + """Should raise an error that gripper offset not found.""" + params = OpenLidParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.get_current_lid_status()).then_return( + AbsorbanceReaderLidStatus.OFF + ) + decoy.when(state_view.modules.get_requested_model(params.moduleId)).then_return( + ModuleModel.ABSORBANCE_READER_V1 + ) + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return() + + decoy.when(state_view.modules.get_location(params.moduleId)).then_return( + DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) + ) + decoy.when( + state_view.modules.ensure_and_convert_module_fixture_location( + DeckSlotName.SLOT_D3, ModuleModel.ABSORBANCE_READER_V1 + ) + ).then_return("absorbanceReaderV1D3") + + decoy.when(state_view.labware.get_absorbance_reader_lid_definition()).then_return( + absorbance_def + ) + decoy.when( + state_view.labware.get_child_gripper_offsets( + labware_definition=absorbance_def, + slot_name=None, + ) + ).then_return(None) + with pytest.raises(ValueError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_read.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_read.py new file mode 100644 index 00000000000..6ba7619f219 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_read.py @@ -0,0 +1,176 @@ +"""Test absorbance reader initilize command.""" +import pytest +from decoy import Decoy + +from opentrons.drivers.types import ABSMeasurementMode, ABSMeasurementConfig +from opentrons.hardware_control.modules import AbsorbanceReader +from opentrons.protocol_engine.errors import ( + CannotPerformModuleAction, + StorageLimitReachedError, +) + +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.resources import FileProvider +from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + AbsorbanceReaderSubState, + AbsorbanceReaderId, +) +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.absorbance_reader import ( + ReadAbsorbanceResult, + ReadAbsorbanceParams, +) +from opentrons.protocol_engine.commands.absorbance_reader.read import ( + ReadAbsorbanceImpl, +) + + +async def test_absorbance_reader_implementation( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + file_provider: FileProvider, +) -> None: + """It should validate, find hardware module if not virtualized, and disengage.""" + subject = ReadAbsorbanceImpl( + state_view=state_view, equipment=equipment, file_provider=file_provider + ) + + params = ReadAbsorbanceParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + verified_module_id = AbsorbanceReaderId("module-id") + asbsorbance_result = {1: {"A1": 1.2}} + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(await absorbance_module_hw.start_measure()).then_return([[1.2, 1.3]]) + decoy.when(absorbance_module_hw._measurement_config).then_return( + ABSMeasurementConfig( + measure_mode=ABSMeasurementMode.SINGLE, + sample_wavelengths=[1, 2], + reference_wavelength=None, + ) + ) + decoy.when( + state_view.modules.convert_absorbance_reader_data_points([1.2, 1.3]) + ).then_return({"A1": 1.2}) + + result = await subject.execute(params=params) + + assert result == SuccessData( + public=ReadAbsorbanceResult( + data=asbsorbance_result, + fileIds=[], + ), + state_update=update_types.StateUpdate( + files_added=update_types.FilesAddedUpdate(file_ids=[]), + absorbance_reader_state_update=update_types.AbsorbanceReaderStateUpdate( + module_id="module-id", + absorbance_reader_data=update_types.AbsorbanceReaderDataUpdate( + read_result=asbsorbance_result + ), + ), + ), + ) + + +async def test_read_raises_cannot_preform_action( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + file_provider: FileProvider, +) -> None: + """It should raise CannotPerformModuleAction when not configured/lid is not on.""" + subject = ReadAbsorbanceImpl( + state_view=state_view, equipment=equipment, file_provider=file_provider + ) + + params = ReadAbsorbanceParams( + moduleId="unverified-module-id", + ) + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + + decoy.when(mabsorbance_module_substate.configured).then_return(False) + + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + decoy.when(mabsorbance_module_substate.configured).then_return(True) + + decoy.when(mabsorbance_module_substate.is_lid_on).then_return(False) + + with pytest.raises(CannotPerformModuleAction): + await subject.execute(params=params) + + +async def test_read_raises_storage_limit( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, + file_provider: FileProvider, +) -> None: + """It should raise StorageLimitReachedError when not configured/lid is not on.""" + subject = ReadAbsorbanceImpl( + state_view=state_view, equipment=equipment, file_provider=file_provider + ) + + params = ReadAbsorbanceParams(moduleId="unverified-module-id", fileName="test") + + mabsorbance_module_substate = decoy.mock(cls=AbsorbanceReaderSubState) + absorbance_module_hw = decoy.mock(cls=AbsorbanceReader) + + verified_module_id = AbsorbanceReaderId("module-id") + + decoy.when( + state_view.modules.get_absorbance_reader_substate("unverified-module-id") + ).then_return(mabsorbance_module_substate) + + decoy.when(mabsorbance_module_substate.module_id).then_return(verified_module_id) + + decoy.when(equipment.get_module_hardware_api(verified_module_id)).then_return( + absorbance_module_hw + ) + decoy.when(await absorbance_module_hw.start_measure()).then_return([[1.2, 1.3]]) + + decoy.when(absorbance_module_hw._measurement_config).then_return( + ABSMeasurementConfig( + measure_mode=ABSMeasurementMode.SINGLE, + sample_wavelengths=[1, 2], + reference_wavelength=None, + ) + ) + decoy.when(mabsorbance_module_substate.configured_wavelengths).then_return( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + ) + + decoy.when(state_view.files.get_filecount()).then_return(390) + with pytest.raises(StorageLimitReachedError): + await subject.execute(params=params) diff --git a/api/tests/opentrons/protocol_engine/commands/conftest.py b/api/tests/opentrons/protocol_engine/commands/conftest.py index 1d27dea0536..cf2d36b092e 100644 --- a/api/tests/opentrons/protocol_engine/commands/conftest.py +++ b/api/tests/opentrons/protocol_engine/commands/conftest.py @@ -15,6 +15,7 @@ TipHandler, GantryMover, ) +from opentrons.protocol_engine.resources import FileProvider from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state.state import StateView @@ -83,3 +84,9 @@ def status_bar(decoy: Decoy) -> StatusBarHandler: def gantry_mover(decoy: Decoy) -> GantryMover: """Get a mocked out GantryMover.""" return decoy.mock(cls=GantryMover) + + +@pytest.fixture +def file_provider(decoy: Decoy) -> FileProvider: + """Get a mocked out StateView.""" + return decoy.mock(cls=FileProvider) diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/__init__.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/__init__.py new file mode 100644 index 00000000000..231be5fd291 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/__init__.py @@ -0,0 +1 @@ +"""Tests for flex stacker commands.""" diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py new file mode 100644 index 00000000000..2a2eda85375 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_retrieve.py @@ -0,0 +1,45 @@ +"""Test Flex Stacker retrieve command implementation.""" +from decoy import Decoy + +from opentrons.hardware_control.modules import FlexStacker + +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + FlexStackerSubState, + FlexStackerId, +) +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.commands import flex_stacker +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.flex_stacker.retrieve import RetrieveImpl + + +async def test_retrieve( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, +) -> None: + """It should be able to retrieve a labware.""" + subject = RetrieveImpl(state_view=state_view, equipment=equipment) + data = flex_stacker.RetrieveParams(moduleId="flex-stacker-id") + + fs_module_substate = decoy.mock(cls=FlexStackerSubState) + fs_hardware = decoy.mock(cls=FlexStacker) + + decoy.when( + state_view.modules.get_flex_stacker_substate(module_id="flex-stacker-id") + ).then_return(fs_module_substate) + + decoy.when(fs_module_substate.module_id).then_return( + FlexStackerId("flex-stacker-id") + ) + + decoy.when( + equipment.get_module_hardware_api(FlexStackerId("flex-stacker-id")) + ).then_return(fs_hardware) + + result = await subject.execute(data) + decoy.verify(await fs_hardware.dispense_labware(labware_height=50.0), times=1) + assert result == SuccessData( + public=flex_stacker.RetrieveResult(), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py new file mode 100644 index 00000000000..e12bde858c2 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_store.py @@ -0,0 +1,45 @@ +"""Test Flex Stacker store command implementation.""" +from decoy import Decoy + +from opentrons.hardware_control.modules import FlexStacker + +from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.module_substates import ( + FlexStackerSubState, + FlexStackerId, +) +from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.commands import flex_stacker +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.flex_stacker.store import StoreImpl + + +async def test_store( + decoy: Decoy, + state_view: StateView, + equipment: EquipmentHandler, +) -> None: + """It should be able to store a labware.""" + subject = StoreImpl(state_view=state_view, equipment=equipment) + data = flex_stacker.StoreParams(moduleId="flex-stacker-id") + + fs_module_substate = decoy.mock(cls=FlexStackerSubState) + fs_hardware = decoy.mock(cls=FlexStacker) + + decoy.when( + state_view.modules.get_flex_stacker_substate(module_id="flex-stacker-id") + ).then_return(fs_module_substate) + + decoy.when(fs_module_substate.module_id).then_return( + FlexStackerId("flex-stacker-id") + ) + + decoy.when( + equipment.get_module_hardware_api(FlexStackerId("flex-stacker-id")) + ).then_return(fs_hardware) + + result = await subject.execute(data) + decoy.verify(await fs_hardware.store_labware(labware_height=50.0), times=1) + assert result == SuccessData( + public=flex_stacker.StoreResult(), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py index 8229d7f4265..c8cdcbec147 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py @@ -6,8 +6,9 @@ import pytest from decoy import Decoy +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from opentrons.types import DeckSlotName -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine.errors import ( LabwareIsNotAllowedInLocationError, diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py index 65ee30e7a88..ae121e9adab 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py @@ -13,7 +13,6 @@ from opentrons.protocol_engine.types import ( DeckSlotLocation, ModuleModel, - ModuleDefinition, ) from opentrons.protocol_engine.execution import EquipmentHandler, LoadedModuleData from opentrons.protocol_engine import ModuleModel as EngineModuleModel @@ -43,107 +42,71 @@ ) +@pytest.mark.parametrize( + "module_model,module_def_fixture_name,load_slot_name", + [ + (ModuleModel.TEMPERATURE_MODULE_V2, "tempdeck_v2_def", DeckSlotName.SLOT_D1), + (ModuleModel.MAGNETIC_BLOCK_V1, "mag_block_v1_def", DeckSlotName.SLOT_D1), + ( + ModuleModel.THERMOCYCLER_MODULE_V2, + "thermocycler_v2_def", + # only B1 provides addressable area for thermocycler v2 + # so we use it here with decoy + DeckSlotName.SLOT_B1, + ), + ( + ModuleModel.HEATER_SHAKER_MODULE_V1, + "heater_shaker_v1_def", + DeckSlotName.SLOT_D3, + ), + (ModuleModel.ABSORBANCE_READER_V1, "abs_reader_v1_def", DeckSlotName.SLOT_D3), + ( + ModuleModel.FLEX_STACKER_MODULE_V1, + "flex_stacker_v1_def", + DeckSlotName.SLOT_D3, + ), + ], +) async def test_load_module_implementation( + request: pytest.FixtureRequest, decoy: Decoy, equipment: EquipmentHandler, state_view: StateView, - tempdeck_v2_def: ModuleDefinition, + ot3_standard_deck_def: DeckDefinitionV5, + module_model: ModuleModel, + module_def_fixture_name: str, + load_slot_name: DeckSlotName, ) -> None: """A loadModule command should have an execution implementation.""" - subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) + module_definition = request.getfixturevalue(module_def_fixture_name) - data = LoadModuleParams( - model=ModuleModel.TEMPERATURE_MODULE_V2, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), - moduleId="some-id", + # Load module function is different for magnetic block v1 + load_module_func = ( + equipment.load_magnetic_block + if module_model == ModuleModel.MAGNETIC_BLOCK_V1 + else equipment.load_module ) - deck_def = load_deck(STANDARD_OT3_DECK, 5) - - decoy.when(state_view.labware.get_deck_definition()).then_return(deck_def) - decoy.when( - state_view.addressable_areas.get_cutout_id_by_deck_slot_name( - DeckSlotName.SLOT_D1 - ) - ).then_return("cutout" + DeckSlotName.SLOT_D1.value) - - decoy.when( - state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) - ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) - - decoy.when( - state_view.modules.ensure_and_convert_module_fixture_location( - deck_slot=data.location.slotName, - model=data.model, - ) - ).then_return(sentinel.addressable_area_provided_by_module) - - decoy.when( - await equipment.load_module( - model=ModuleModel.TEMPERATURE_MODULE_V2, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), - module_id="some-id", - ) - ).then_return( - LoadedModuleData( - module_id="module-id", - serial_number="mod-serial", - definition=tempdeck_v2_def, - ) - ) - - result = await subject.execute(data) - decoy.verify( - state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - sentinel.addressable_area_provided_by_module - ) - ) - assert result == SuccessData( - public=LoadModuleResult( - moduleId="module-id", - serialNumber="mod-serial", - model=ModuleModel.TEMPERATURE_MODULE_V2, - definition=tempdeck_v2_def, - ), - state_update=update_types.StateUpdate( - addressable_area_used=update_types.AddressableAreaUsedUpdate( - addressable_area_name=data.location.slotName.id - ) - ), - ) - - -async def test_load_module_implementation_mag_block( - decoy: Decoy, - equipment: EquipmentHandler, - state_view: StateView, - mag_block_v1_def: ModuleDefinition, -) -> None: - """A loadModule command for mag block should have an execution implementation.""" subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) data = LoadModuleParams( - model=ModuleModel.MAGNETIC_BLOCK_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), + model=module_model, + location=DeckSlotLocation(slotName=load_slot_name), moduleId="some-id", ) - deck_def = load_deck(STANDARD_OT3_DECK, 5) - - decoy.when(state_view.labware.get_deck_definition()).then_return(deck_def) + decoy.when(state_view.labware.get_deck_definition()).then_return( + ot3_standard_deck_def + ) decoy.when( - state_view.addressable_areas.get_cutout_id_by_deck_slot_name( - DeckSlotName.SLOT_D1 - ) - ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + state_view.addressable_areas.get_cutout_id_by_deck_slot_name(load_slot_name) + ).then_return("cutout" + load_slot_name.value) decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) + DeckSlotLocation(slotName=load_slot_name) ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) + ).then_return(DeckSlotLocation(slotName=load_slot_name)) decoy.when( state_view.modules.ensure_and_convert_module_fixture_location( @@ -153,16 +116,16 @@ async def test_load_module_implementation_mag_block( ).then_return(sentinel.addressable_area_provided_by_module) decoy.when( - await equipment.load_magnetic_block( - model=ModuleModel.MAGNETIC_BLOCK_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + await load_module_func( + model=module_model, + location=DeckSlotLocation(slotName=load_slot_name), module_id="some-id", ) ).then_return( LoadedModuleData( module_id="module-id", - serial_number=None, - definition=mag_block_v1_def, + serial_number="mod-serial", + definition=module_definition, ) ) @@ -175,76 +138,9 @@ async def test_load_module_implementation_mag_block( assert result == SuccessData( public=LoadModuleResult( moduleId="module-id", - serialNumber=None, - model=ModuleModel.MAGNETIC_BLOCK_V1, - definition=mag_block_v1_def, - ), - state_update=update_types.StateUpdate( - addressable_area_used=update_types.AddressableAreaUsedUpdate( - addressable_area_name=data.location.slotName.id - ) - ), - ) - - -async def test_load_module_implementation_abs_reader( - decoy: Decoy, - equipment: EquipmentHandler, - state_view: StateView, - abs_reader_v1_def: ModuleDefinition, -) -> None: - """A loadModule command for abs reader should have an execution implementation.""" - subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) - - data = LoadModuleParams( - model=ModuleModel.ABSORBANCE_READER_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), - moduleId="some-id", - ) - - deck_def = load_deck(STANDARD_OT3_DECK, 5) - - decoy.when(state_view.labware.get_deck_definition()).then_return(deck_def) - decoy.when( - state_view.addressable_areas.get_cutout_id_by_deck_slot_name( - DeckSlotName.SLOT_D3 - ) - ).then_return("cutout" + DeckSlotName.SLOT_D3.value) - - decoy.when( - state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_D3) - ) - ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_D3)) - - decoy.when( - state_view.modules.ensure_and_convert_module_fixture_location( - deck_slot=data.location.slotName, - model=data.model, - ) - ).then_return(sentinel.addressable_area_name) - - decoy.when( - await equipment.load_module( - model=ModuleModel.ABSORBANCE_READER_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), - module_id="some-id", - ) - ).then_return( - LoadedModuleData( - module_id="module-id", - serial_number=None, - definition=abs_reader_v1_def, - ) - ) - - result = await subject.execute(data) - assert result == SuccessData( - public=LoadModuleResult( - moduleId="module-id", - serialNumber=None, - model=ModuleModel.ABSORBANCE_READER_V1, - definition=abs_reader_v1_def, + serialNumber="mod-serial", + model=module_model, + definition=module_definition, ), state_update=update_types.StateUpdate( addressable_area_used=update_types.AddressableAreaUsedUpdate( diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index 2036bda558a..3a1e8c47b02 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -7,7 +7,11 @@ import pytest from decoy import Decoy, matchers -from opentrons_shared_data.labware.labware_definition import Parameters, Dimensions +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + Parameters, + Dimensions, +) from opentrons_shared_data.errors.exceptions import ( EnumeratedError, FailedGripperPickupError, @@ -18,7 +22,6 @@ from opentrons.protocol_engine.state import update_types from opentrons.types import DeckSlotName, Point -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import errors, Config from opentrons.protocol_engine.resources import labware_validation from opentrons.protocol_engine.resources.model_utils import ModelUtils diff --git a/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py b/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py index 51779c427d7..6b13f464e77 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py @@ -1,15 +1,12 @@ """Test load labware commands.""" import inspect -from opentrons.protocol_engine.state.update_types import ( - LabwareLocationUpdate, - StateUpdate, -) -import pytest +import pytest from decoy import Decoy +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from opentrons.types import DeckSlotName -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine.errors import ( LabwareNotLoadedError, @@ -21,6 +18,10 @@ from opentrons.protocol_engine.execution import ReloadedLabwareData, EquipmentHandler from opentrons.protocol_engine.resources import labware_validation from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.update_types import ( + LabwareLocationUpdate, + StateUpdate, +) from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.reload_labware import ( diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index 48ce28e7a98..acc7d2e8829 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -9,8 +9,8 @@ from opentrons_shared_data.deck import load as load_deck from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.labware import load_definition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.pipette import pipette_definition -from opentrons.protocols.models import LabwareDefinition from opentrons.protocols.api_support.deck_type import ( STANDARD_OT2_DECK, SHORT_TRASH_DECK, @@ -234,6 +234,13 @@ def abs_reader_v1_def() -> ModuleDefinition: return ModuleDefinition.model_validate_json(definition) +@pytest.fixture(scope="session") +def flex_stacker_v1_def() -> ModuleDefinition: + """Get the definition of a V1 Flex Stacker.""" + definition = load_shared_data("module/definitions/3/flexStackerModuleV1.json") + return ModuleDefinition.model_validate_json(definition) + + @pytest.fixture(scope="session") def supported_tip_fixture() -> pipette_definition.SupportedTipsDefinition: """Get a mock supported tip definition.""" diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index 29117a894b5..5d3edc307bd 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -9,6 +9,7 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.pipette import pipette_definition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.types import LabwareUri from opentrons.calibration_storage.helpers import uri_from_details @@ -21,7 +22,6 @@ AbstractModule, ) from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import errors from opentrons.protocol_engine.types import ( diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 23f701db80b..b04bdd25b67 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -11,7 +11,6 @@ from opentrons.hardware_control.types import TipStateType from opentrons.hardware_control.protocols.types import OT2RobotType, FlexRobotType -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import TipGeometry, TipPresenceStatus from opentrons.protocol_engine.resources import LabwareDataProvider @@ -20,6 +19,7 @@ CommandPreconditionViolated, CommandParameterLimitViolated, ) +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons.protocol_engine.execution.tip_handler import ( HardwareTipHandler, VirtualTipHandler, diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py index 174c101f8b1..ba9c93e03cf 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -203,6 +203,13 @@ def test_get_provided_addressable_area_names( cutout_fixture_id="stagingAreaRightSlot", provided_addressable_areas=frozenset({"D3", "D4"}), ), + PotentialCutoutFixture( + cutout_id="cutoutD3", + cutout_fixture_id="flexStackerModuleV1", + provided_addressable_areas=frozenset( + {"D3", "flexStackerModuleV1D4"} + ), + ), }, lazy_fixture("ot3_standard_deck_def"), ), diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py index e051f155113..2c6ed1589dd 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py @@ -4,7 +4,7 @@ from decoy import Decoy from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons.types import DeckSlotName from opentrons.protocol_engine.types import ( diff --git a/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py index a666e7a697d..80c9775ad2d 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py @@ -1,9 +1,9 @@ """Functional tests for the LabwareDataProvider.""" from typing import cast +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict from opentrons.calibration_storage.helpers import hash_labware_def -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_api.labware import get_labware_definition from opentrons.protocol_engine.resources import LabwareDataProvider diff --git a/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py b/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py index fbe9d6c21a4..bb0db528598 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py +++ b/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py @@ -2,11 +2,11 @@ import pytest from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, LabwareRole, OverlapOffset, Parameters, ) -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine.resources import labware_validation as subject diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index 31f06858b53..a50d6ebc1b4 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -4,9 +4,9 @@ from pydantic import BaseModel from typing import Optional, cast, Dict +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.types import MountType -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import ErrorOccurrence, commands as cmd from opentrons.protocol_engine.types import ( DeckPoint, diff --git a/api/tests/opentrons/protocol_engine/state/inner_geometry_test_params.py b/api/tests/opentrons/protocol_engine/state/inner_geometry_test_params.py new file mode 100644 index 00000000000..0a60bf4206c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/inner_geometry_test_params.py @@ -0,0 +1,150 @@ +"""Arguments needed to test inner geometry. + +Each labware has 2 nominal volumes calculated in solidworks. +- One is a nominal bottom volume, calculated some set distance from the bottom of the inside of the well. +- The other is a nominal top volume, calculated some set distance from the top of the inside of the well. +""" +INNER_WELL_GEOMETRY_TEST_PARAMS = [ + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell15mL", + 16.7, + 15546.9, + 3.0, + 5.0, + ], + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell50mL", + 111.2, + 56110.3, + 3.0, + 5.0, + ], + ["opentrons_24_tuberack_nest_2ml_screwcap", "conicalWell", 66.6, 2104.9, 3.0, 3.0], + [ + "opentrons_24_tuberack_nest_1.5ml_screwcap", + "conicalWell", + 19.5, + 1750.8, + 3.0, + 3.0, + ], + ["nest_1_reservoir_290ml", "cuboidalWell", 16570.380, 271690.520, 3.0, 3.0], + ["opentrons_24_tuberack_nest_2ml_snapcap", "conicalWell", 69.62, 2148.5, 3.0, 3.0], + ["nest_96_wellplate_2ml_deep", "cuboidalWell", 118.3, 2060.4, 3.0, 3.0], + ["opentrons_24_tuberack_nest_1.5ml_snapcap", "conicalWell", 27.8, 1682.3, 3.0, 3.0], + ["nest_12_reservoir_15ml", "cuboidalWell", 1219.0, 13236.1, 3.0, 3.0], + ["nest_1_reservoir_195ml", "cuboidalWell", 14034.2, 172301.9, 3.0, 3.0], + [ + "opentrons_24_tuberack_nest_0.5ml_screwcap", + "conicalWell", + 21.95, + 795.4, + 3.0, + 3.0, + ], + [ + "opentrons_96_wellplate_200ul_pcr_full_skirt", + "conicalWell", + 14.3, + 150.2, + 3.0, + 3.0, + ], + ["nest_96_wellplate_100ul_pcr_full_skirt", "conicalWell", 15.5, 150.8, 3.0, 3.0], + ["nest_96_wellplate_200ul_flat", "conicalWell", 96.3, 259.8, 3.0, 3.0], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "50mlconicalWell", + 163.9, + 57720.5, + 3.0, + 3.0, + ], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "15mlconicalWell", + 40.8, + 15956.6, + 3.0, + 3.0, + ], + ["usascientific_12_reservoir_22ml", "cuboidalWell", 529.36, 21111.5, 3.0, 3.0], + ["thermoscientificnunc_96_wellplate_2000ul", "conicalWell", 73.5, 1768.0, 3.0, 3.0], + [ + "usascientific_96_wellplate_2.4ml_deep", + "cuboidalWell", + 72.220, + 2241.360, + 3.0, + 3.0, + ], + ["agilent_1_reservoir_290ml", "cuboidalWell", 15652.9, 268813.8, 3.0, 3.0], + [ + "opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap", + "conicalWell", + 25.8, + 1576.1, + 3.0, + 3.0, + ], + ["thermoscientificnunc_96_wellplate_1300ul", "conicalWell", 73.5, 1155.1, 3.0, 3.0], + ["corning_12_wellplate_6.9ml_flat", "conicalWell", 1156.3, 5654.8, 3.0, 3.0], + ["corning_24_wellplate_3.4ml_flat", "conicalWell", 579.0, 2853.4, 3.0, 3.0], + ["corning_6_wellplate_16.8ml_flat", "conicalWell", 2862.1, 13901.9, 3.0, 3.0], + ["corning_48_wellplate_1.6ml_flat", "conicalWell", 268.9, 1327.0, 3.0, 3.0], + ["biorad_96_wellplate_200ul_pcr", "conicalWell", 17.9, 161.2, 3.0, 3.0], + ["axygen_1_reservoir_90ml", "cuboidalWell", 22373.4, 70450.6, 3.0, 3.0], + ["corning_384_wellplate_112ul_flat", "flatWell", 22.4, 77.4, 2.88, 3.0], + ["corning_96_wellplate_360ul_flat", "conicalWell", 97.2, 257.1, 3.0, 3.0], + ["biorad_384_wellplate_50ul", "conicalWell", 7.7, 27.8, 3.0, 3.0], + [ + "appliedbiosystemsmicroamp_384_wellplate_40ul", + "conicalWell", + 7.44, + 26.19, + 3.0, + 3.0, + ], + [ + "opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap", + "conicalWell", + 60.940, + 2163.980, + 3.0, + 3.0, + ], + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell15mL", + 16.690, + 15546.930, + 3.0, + 5.0, + ], + [ + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", + "conicalWell50mL", + 111.200, + 56110.279, + 3.0, + 5.0, + ], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "15mlconicalWell", + 40.830, + 15956.600, + 3.0, + 3.0, + ], + [ + "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", + "50mlconicalWell", + 163.860, + 57720.510, + 3.0, + 3.0, + ], +] diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index fda32a56ce0..bf82c17c6bc 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -6,6 +6,7 @@ from math import isclose from typing import cast, List, Tuple, Optional, NamedTuple, Dict from unittest.mock import sentinel +from os import listdir, path import pytest from decoy import Decoy @@ -15,20 +16,21 @@ StateUpdate, ) +from opentrons_shared_data import get_shared_data_root, load_shared_data from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.deck import load as load_deck +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.types import LabwareUri from opentrons_shared_data.pipette import pipette_definition from opentrons.calibration_storage.helpers import uri_from_details -from opentrons.protocols.models import LabwareDefinition from opentrons.types import Point, DeckSlotName, MountType, StagingSlotName from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.labware.labware_definition import ( Dimensions as LabwareDimensions, Parameters as LabwareDefinitionParameters, CornerOffsetFromSlot, + ConicalFrustum, ) -from opentrons_shared_data import load_shared_data from opentrons.protocol_engine import errors from opentrons.protocol_engine.types import ( @@ -96,6 +98,7 @@ _volume_from_height_circular, _volume_from_height_rectangular, ) +from .inner_geometry_test_params import INNER_WELL_GEOMETRY_TEST_PARAMS from ..pipette_fixtures import get_default_nozzle_map from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES from ..mock_rectangular_frusta import TEST_EXAMPLES as RECTANGULAR_TEST_EXAMPLES @@ -3278,19 +3281,23 @@ def _find_volume_from_height_(index: int) -> None: nonlocal total_frustum_height, bottom_radius top_radius = frustum["radius"][index] target_height = frustum["height"][index] - + segment = ConicalFrustum( + shape="conical", + bottomDiameter=bottom_radius * 2, + topDiameter=top_radius * 2, + topHeight=total_frustum_height, + bottomHeight=0.0, + xCount=1, + yCount=1, + ) found_volume = _volume_from_height_circular( target_height=target_height, - total_frustum_height=total_frustum_height, - top_radius=top_radius, - bottom_radius=bottom_radius, + segment=segment, ) found_height = _height_from_volume_circular( - volume=found_volume, - total_frustum_height=total_frustum_height, - top_radius=top_radius, - bottom_radius=bottom_radius, + target_volume=found_volume, + segment=segment, ) assert isclose(found_height, frustum["height"][index]) @@ -3360,3 +3367,133 @@ def test_validate_dispense_volume_into_well_meniscus( ), volume=1100000.0, ) + + +@pytest.mark.parametrize( + [ + "labware_id", + "well_name", + "input_volume_bottom", + "input_volume_top", + "expected_height_from_bottom_mm", + "expected_height_from_top_mm", + ], + INNER_WELL_GEOMETRY_TEST_PARAMS, +) +def test_get_well_height_at_volume( + decoy: Decoy, + subject: GeometryView, + labware_id: str, + well_name: str, + input_volume_bottom: float, + input_volume_top: float, + expected_height_from_bottom_mm: float, + expected_height_from_top_mm: float, + mock_labware_view: LabwareView, +) -> None: + """Test getting the well height at a given volume.""" + + def _get_labware_def() -> LabwareDefinition: + def_dir = str(get_shared_data_root()) + f"/labware/definitions/3/{labware_id}" + version_str = max([str(version) for version in listdir(def_dir)]) + def_path = path.join(def_dir, version_str) + _labware_def = LabwareDefinition.model_validate( + json.loads(load_shared_data(def_path).decode("utf-8")) + ) + return _labware_def + + labware_def = _get_labware_def() + assert labware_def.innerLabwareGeometry is not None + well_geometry = labware_def.innerLabwareGeometry.get(well_name) + assert well_geometry is not None + well_definition = [ + well + for well in labware_def.wells.values() + if well.geometryDefinitionId == well_name + ][0] + + decoy.when(mock_labware_view.get_well_geometry(labware_id, well_name)).then_return( + well_geometry + ) + decoy.when( + mock_labware_view.get_well_definition(labware_id, well_name) + ).then_return(well_definition) + + found_height_bottom = subject.get_well_height_at_volume( + labware_id=labware_id, well_name=well_name, volume=input_volume_bottom + ) + found_height_top = subject.get_well_height_at_volume( + labware_id=labware_id, well_name=well_name, volume=input_volume_top + ) + assert isclose(found_height_bottom, expected_height_from_bottom_mm, rel_tol=0.01) + vol_2_expected_height_from_bottom = ( + subject.get_well_height(labware_id=labware_id, well_name=well_name) + - expected_height_from_top_mm + ) + assert isclose(found_height_top, vol_2_expected_height_from_bottom, rel_tol=0.01) + + +@pytest.mark.parametrize( + [ + "labware_id", + "well_name", + "expected_volume_bottom", + "expected_volume_top", + "input_height_from_bottom_mm", + "input_height_from_top_mm", + ], + INNER_WELL_GEOMETRY_TEST_PARAMS, +) +def test_get_well_volume_at_height( + decoy: Decoy, + subject: GeometryView, + labware_id: str, + well_name: str, + expected_volume_bottom: float, + expected_volume_top: float, + input_height_from_bottom_mm: float, + input_height_from_top_mm: float, + mock_labware_view: LabwareView, +) -> None: + """Test getting the volume at a given height.""" + + def _get_labware_def() -> LabwareDefinition: + def_dir = str(get_shared_data_root()) + f"/labware/definitions/3/{labware_id}" + version_str = max([str(version) for version in listdir(def_dir)]) + def_path = path.join(def_dir, version_str) + _labware_def = LabwareDefinition.model_validate( + json.loads(load_shared_data(def_path).decode("utf-8")) + ) + return _labware_def + + labware_def = _get_labware_def() + assert labware_def.innerLabwareGeometry is not None + well_geometry = labware_def.innerLabwareGeometry.get(well_name) + assert well_geometry is not None + well_definition = [ + well + for well in labware_def.wells.values() + if well.geometryDefinitionId == well_name + ][0] + + decoy.when(mock_labware_view.get_well_geometry(labware_id, well_name)).then_return( + well_geometry + ) + decoy.when( + mock_labware_view.get_well_definition(labware_id, well_name) + ).then_return(well_definition) + + found_volume_bottom = subject.get_well_volume_at_height( + labware_id=labware_id, well_name=well_name, height=input_height_from_bottom_mm + ) + vol_2_input_height_from_bottom = ( + subject.get_well_height(labware_id=labware_id, well_name=well_name) + - input_height_from_top_mm + ) + found_volume_top = subject.get_well_volume_at_height( + labware_id=labware_id, + well_name=well_name, + height=vol_2_input_height_from_bottom, + ) + assert isclose(found_volume_bottom, expected_volume_bottom, rel_tol=0.01) + assert isclose(found_volume_top, expected_volume_top, rel_tol=0.01) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py index 3b539df58e3..d5e7e41770e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_store_old.py @@ -12,7 +12,7 @@ from opentrons.calibration_storage.helpers import uri_from_details from opentrons_shared_data.deck.types import DeckDefinitionV5 -from opentrons.protocols.models import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons.types import DeckSlotName from opentrons.protocol_engine.types import ( diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py index 7ace6d767ad..c657ec2a7ae 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -15,6 +15,7 @@ from opentrons_shared_data.labware import load_definition from opentrons_shared_data.labware.labware_definition import ( Parameters, + LabwareDefinition, LabwareRole, OverlapOffset as SharedDataOverlapOffset, GripperOffsets, @@ -25,7 +26,6 @@ STANDARD_OT2_DECK, STANDARD_OT3_DECK, ) -from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName, MountType from opentrons.protocol_engine import errors @@ -1414,25 +1414,25 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: @pytest.mark.parametrize( argnames=[ "allowed_roles", - "stacking_quirks", + "stack_limit", "exception", ], argvalues=[ [ [LabwareRole.labware], - [], + 1, pytest.raises(errors.LabwareCannotBeStackedError), ], [ [LabwareRole.lid], - ["stackingMaxFive"], + 5, does_not_raise(), ], ], ) def test_labware_stacking_height_passes_or_raises( allowed_roles: List[LabwareRole], - stacking_quirks: List[str], + stack_limit: int, exception: ContextManager[None], ) -> None: """It should raise if the labware is stacked too high, and pass if the labware definition allowed this.""" @@ -1468,11 +1468,11 @@ def test_labware_stacking_height_passes_or_raises( allowedRoles=allowed_roles, parameters=Parameters.model_construct( format="irregular", - quirks=stacking_quirks, isTiprack=False, loadName="name", isMagneticModuleCompatible=False, ), + stackLimit=stack_limit, ) }, ) @@ -1482,7 +1482,6 @@ def test_labware_stacking_height_passes_or_raises( top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] parameters=Parameters.model_construct( format="irregular", - quirks=stacking_quirks, isTiprack=False, loadName="name", isMagneticModuleCompatible=False, @@ -1490,6 +1489,7 @@ def test_labware_stacking_height_passes_or_raises( stackingOffsetWithLabware={ "test": SharedDataOverlapOffset(x=0, y=0, z=0) }, + stackLimit=stack_limit, ), bottom_labware_id="labware-id4", ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store_old.py b/api/tests/opentrons/protocol_engine/state/test_module_store_old.py index 4767ecad16b..b3125865ba6 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store_old.py @@ -7,6 +7,8 @@ from typing import List, Set, cast, Dict, Optional import pytest + +from opentrons.protocol_engine.state import update_types from opentrons_shared_data.robot.types import RobotType from opentrons_shared_data.deck.types import DeckDefinitionV5 from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] @@ -44,6 +46,8 @@ TemperatureModuleSubState, ThermocyclerModuleId, ThermocyclerModuleSubState, + AbsorbanceReaderSubState, + AbsorbanceReaderId, ModuleSubStateType, ) @@ -723,3 +727,134 @@ def test_handle_thermocycler_lid_commands( target_lid_temperature=None, ) } + + +def test_handle_absorbance_reader_commands( + abs_reader_v1_def: ModuleDefinition, +) -> None: + """It should update absorbance reader state.""" + load_module_cmd = commands.LoadModule.model_construct( # type: ignore[call-arg] + params=commands.LoadModuleParams( + model=ModuleModel.ABSORBANCE_READER_V1, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + ), + result=commands.LoadModuleResult( + moduleId="module-id", + model=ModuleModel.ABSORBANCE_READER_V1, + serialNumber="serial-number", + definition=abs_reader_v1_def, + ), + ) + + initialize_reader = commands.Comment.model_construct( # type: ignore[call-arg] + params=commands.CommentParams(message="hello"), + result=commands.CommentResult(), + ) + open_lid = commands.Comment.model_construct( # type: ignore[call-arg] + params=commands.CommentParams(message="hello dude"), + result=commands.CommentResult(), + ) + + read_data = commands.Comment.model_construct( # type: ignore[call-arg] + params=commands.CommentParams(message="hello man"), + result=commands.CommentResult(), + ) + + close_lid = commands.Comment.model_construct( # type: ignore[call-arg] + params=commands.CommentParams(message="hello ladies"), + result=commands.CommentResult(), + ) + + subject = ModuleStore( + Config( + use_simulated_deck_config=False, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_fixed_labware=[], + ) + + subject.handle_action( + actions.SucceedCommandAction( + command=load_module_cmd, + state_update=update_types.StateUpdate().initialize_absorbance_reader( + "module-id", "single", [1], None + ), + ) + ) + subject.handle_action(actions.SucceedCommandAction(command=initialize_reader)) + assert subject.state.substate_by_module_id == { + "module-id": AbsorbanceReaderSubState( + module_id=AbsorbanceReaderId("module-id"), + is_lid_on=True, + configured=True, + measured=False, + data=None, + configured_wavelengths=[1], + measure_mode="single", # type: ignore[arg-type] + reference_wavelength=None, + ) + } + + subject.handle_action( + actions.SucceedCommandAction( + command=open_lid, + state_update=update_types.StateUpdate().set_absorbance_reader_lid( + module_id="module-id", is_lid_on=False + ), + ) + ) + assert subject.state.substate_by_module_id == { + "module-id": AbsorbanceReaderSubState( + module_id=AbsorbanceReaderId("module-id"), + is_lid_on=False, + configured=True, + measured=True, + data=None, + configured_wavelengths=[1], + measure_mode="single", # type: ignore[arg-type] + reference_wavelength=None, + ) + } + + subject.handle_action( + actions.SucceedCommandAction( + command=read_data, + state_update=update_types.StateUpdate().set_absorbance_reader_data( + module_id="module-id", read_result={1: {"A1": 1.2}} + ), + ) + ) + assert subject.state.substate_by_module_id == { + "module-id": AbsorbanceReaderSubState( + module_id=AbsorbanceReaderId("module-id"), + is_lid_on=False, + configured=True, + measured=True, + data={1: {"A1": 1.2}}, + configured_wavelengths=[1], + measure_mode="single", # type: ignore[arg-type] + reference_wavelength=None, + ) + } + + subject.handle_action( + actions.SucceedCommandAction( + command=close_lid, + state_update=update_types.StateUpdate().set_absorbance_reader_lid( + module_id="module-id", is_lid_on=True + ), + ) + ) + assert subject.state.substate_by_module_id == { + "module-id": AbsorbanceReaderSubState( + module_id=AbsorbanceReaderId("module-id"), + is_lid_on=True, + configured=True, + measured=True, + data={1: {"A1": 1.2}}, + configured_wavelengths=[1], + measure_mode="single", # type: ignore[arg-type] + reference_wavelength=None, + ) + } diff --git a/api/tests/opentrons/protocol_engine/state/test_update_types.py b/api/tests/opentrons/protocol_engine/state/test_update_types.py index 741df813e19..325f2611f37 100644 --- a/api/tests/opentrons/protocol_engine/state/test_update_types.py +++ b/api/tests/opentrons/protocol_engine/state/test_update_types.py @@ -1,14 +1,16 @@ """Unit tests for the utilities in `update_types`.""" - - +from opentrons.protocol_engine import DeckSlotLocation, ModuleLocation from opentrons.protocol_engine.state import update_types +from opentrons.types import DeckSlotName def test_append() -> None: """Test `StateUpdate.append()`.""" state_update = update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=True + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, ) ) @@ -17,22 +19,28 @@ def test_append() -> None: update_types.StateUpdate(pipette_location=update_types.CLEAR) ) assert result is state_update - assert state_update.absorbance_reader_lid == update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=True + assert state_update.labware_location == update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, ) assert state_update.pipette_location == update_types.CLEAR # Populating a field that's already been populated should overwrite it. result = state_update.append( update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=False + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, ) ) ) assert result is state_update - assert state_update.absorbance_reader_lid == update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=False + assert state_update.labware_location == update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, ) assert state_update.pipette_location == update_types.CLEAR @@ -44,14 +52,18 @@ def test_reduce() -> None: # It should union all the set fields together. assert update_types.StateUpdate.reduce( update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=True + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, ) ), update_types.StateUpdate(pipette_location=update_types.CLEAR), ) == update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=True + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, ), pipette_location=update_types.CLEAR, ) @@ -59,17 +71,23 @@ def test_reduce() -> None: # When one field appears multiple times, the last write wins. assert update_types.StateUpdate.reduce( update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=True + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + offset_id=None, ) ), update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=False + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, ) ), ) == update_types.StateUpdate( - absorbance_reader_lid=update_types.AbsorbanceReaderLidUpdate( - module_id="module_id", is_lid_on=False + labware_location=update_types.LabwareLocationUpdate( + labware_id="test-123", + new_location=ModuleLocation(moduleId="module-123"), + offset_id=None, ) ) diff --git a/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py index b5733cda6b8..a7cb708df75 100644 --- a/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py @@ -2,14 +2,14 @@ import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] +from opentrons_shared_data.deck import load as load_deck from opentrons_shared_data.deck.types import DeckDefinitionV5 +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.robot.types import RobotType -from opentrons_shared_data.deck import load as load_deck from opentrons.calibration_storage.helpers import uri_from_details from opentrons.hardware_control import API as HardwareAPI from opentrons.hardware_control.types import DoorState -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import ( ProtocolEngine, Config as EngineConfig, diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 95289d681b8..6c1efcc55d7 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -9,6 +9,7 @@ from decoy import Decoy from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons.protocol_engine.actions.actions import SetErrorRecoveryPolicyAction from opentrons.protocol_engine.state.update_types import StateUpdate @@ -16,7 +17,6 @@ from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI from opentrons.hardware_control.modules import MagDeck, TempDeck from opentrons.hardware_control.types import PauseType as HardwarePauseType -from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import ProtocolEngine, commands, slot_standardization from opentrons.protocol_engine.errors.exceptions import ( diff --git a/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py b/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py index 644c2b7094f..b80667a302b 100644 --- a/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py +++ b/api/tests/opentrons/protocols/advanced_control/transfers/test_common_functions.py @@ -44,20 +44,20 @@ def test_check_valid_volume_parameters( argvalues=[ ( [60, 70, 75], - [("a", "b"), ("c", "d"), ("e", "f")], + [(("a", "b"), (1, 2)), (("c", "d"), (3, 4)), (("e", "f"), (5, 6))], 20, [ - (20, ("a", "b")), - (20, ("a", "b")), - (20, ("a", "b")), - (20, ("c", "d")), - (20, ("c", "d")), - (15, ("c", "d")), - (15, ("c", "d")), - (20, ("e", "f")), - (20, ("e", "f")), - (17.5, ("e", "f")), - (17.5, ("e", "f")), + (20, (("a", "b"), (1, 2))), + (20, (("a", "b"), (1, 2))), + (20, (("a", "b"), (1, 2))), + (20, (("c", "d"), (3, 4))), + (20, (("c", "d"), (3, 4))), + (15, (("c", "d"), (3, 4))), + (15, (("c", "d"), (3, 4))), + (20, (("e", "f"), (5, 6))), + (20, (("e", "f"), (5, 6))), + (17.5, (("e", "f"), (5, 6))), + (17.5, (("e", "f"), (5, 6))), ], ), ], diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 0b8d3429527..d9bd8173834 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -11,7 +11,6 @@ _cross_section_area_rectangular, _cross_section_area_circular, _reject_unacceptable_heights, - _circular_frustum_polynomial_roots, _rectangular_frustum_polynomial_roots, _volume_from_height_rectangular, _volume_from_height_circular, @@ -211,39 +210,25 @@ def test_volume_and_height_circular(well: List[Any]) -> None: """Test both volume and height calculations for circular frusta.""" if well[-1].shape == "spherical": return - total_height = well[0].topHeight for segment in well: if segment.shape == "conical": - top_radius = segment.topDiameter / 2 - bottom_radius = segment.bottomDiameter / 2 - a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2) - b = pi * bottom_radius * (top_radius - bottom_radius) / total_height - c = pi * bottom_radius**2 - assert _circular_frustum_polynomial_roots( - top_radius=top_radius, - bottom_radius=bottom_radius, - total_frustum_height=total_height, - ) == (a, b, c) + a = segment.topDiameter / 2 + b = segment.bottomDiameter / 2 # test volume within a bunch of arbitrary heights - for target_height in range(round(total_height)): - expected_volume = ( - a * (target_height**3) - + b * (target_height**2) - + c * target_height + segment_height = segment.topHeight - segment.bottomHeight + for target_height in range(round(segment_height)): + r_y = (target_height / segment_height) * (a - b) + b + expected_volume = (pi * target_height / 3) * ( + b**2 + b * r_y + r_y**2 ) found_volume = _volume_from_height_circular( target_height=target_height, - total_frustum_height=total_height, - bottom_radius=bottom_radius, - top_radius=top_radius, + segment=segment, ) - assert found_volume == expected_volume + assert isclose(found_volume, expected_volume) # test going backwards to get height back found_height = _height_from_volume_circular( - volume=found_volume, - total_frustum_height=total_height, - bottom_radius=bottom_radius, - top_radius=top_radius, + target_volume=found_volume, segment=segment ) assert isclose(found_height, target_height) diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 6e8c78449f0..5337965fc52 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -9,7 +9,6 @@ import { COLORS, OVERFLOW_AUTO, POSITION_RELATIVE, - useIdle, useScrolling, } from '@opentrons/components' import { ApiHostProvider } from '@opentrons/react-api-client' @@ -52,7 +51,7 @@ import { updateConfigValue, } from '/app/redux/config' import { updateBrightness } from '/app/redux/shell' -import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import { useScreenIdle, SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' import { useProtocolReceiptToast, useSoftwareUpdatePoll } from './hooks' import { ODDTopLevelRedirects } from './ODDTopLevelRedirects' import { ReactQueryDevtools } from '/app/App/tools' @@ -166,7 +165,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { initialState: false, } const dispatch = useDispatch() - const isIdle = useIdle(sleepTime, options) + const isIdle = useScreenIdle(sleepTime, options) useEffect(() => { if (isIdle) { diff --git a/app/src/DesignTokens/Spacing/Spacing.stories.tsx b/app/src/DesignTokens/Spacing/Spacing.stories.tsx index 3b12ae833db..11c9125a45f 100644 --- a/app/src/DesignTokens/Spacing/Spacing.stories.tsx +++ b/app/src/DesignTokens/Spacing/Spacing.stories.tsx @@ -1,3 +1,6 @@ +import styled from 'styled-components' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react' import { ALIGN_FLEX_START, Box, @@ -5,8 +8,7 @@ import { DIRECTION_COLUMN, Flex, SPACING, - LegacyStyledText, - TYPOGRAPHY, + StyledText, } from '@opentrons/components' import type { Story, Meta } from '@storybook/react' @@ -21,6 +23,12 @@ interface SpacingsStorybookProps { const Template: Story = args => { const targetSpacings = args.spacings.filter(s => !s[1].includes('auto')) + // sort by rem value + const sortedSpacing = targetSpacings.sort((a, b) => { + const aValue = parseFloat(a[1].replace('rem', '')) + const bValue = parseFloat(b[1].replace('rem', '')) + return aValue - bValue + }) const convertToPx = (remFormat: string): string => { const pxVal = Number(remFormat.replace('rem', '')) * 16 @@ -33,7 +41,7 @@ const Template: Story = args => { gridGap={SPACING.spacing8} padding={SPACING.spacing24} > - {targetSpacings.map((spacing, index) => ( + {sortedSpacing.map((spacing, index) => ( = args => { width="100%" height="6rem" > - + {`${spacing[0]} - ${spacing[1]}: ${convertToPx(spacing[1])}`} - - + + + + + ))} @@ -62,3 +69,9 @@ const allSpacings = Object.entries(SPACING) AllSpacing.args = { spacings: allSpacings, } + +const StyledBox = styled(Box)` + width: 2rem; + height: 2rem; + background-color: ${COLORS.blue35}; +` diff --git a/app/src/local-resources/config/constants.ts b/app/src/local-resources/config/constants.ts deleted file mode 100644 index 1c7b0e2727d..00000000000 --- a/app/src/local-resources/config/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const SLEEP_NEVER_MS = 604800000 diff --git a/app/src/local-resources/config/index.ts b/app/src/local-resources/config/index.ts deleted file mode 100644 index f87cf0102a1..00000000000 --- a/app/src/local-resources/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './constants' diff --git a/app/src/local-resources/dom-utils/constants.ts b/app/src/local-resources/dom-utils/constants.ts new file mode 100644 index 00000000000..f48e8566837 --- /dev/null +++ b/app/src/local-resources/dom-utils/constants.ts @@ -0,0 +1,2 @@ +// See RQA-3813 and associated PR. We are stuck using this hardcoded number to mean "never" or migrate the on-filesystem value. +export const SLEEP_NEVER_MS = 604800000 diff --git a/components/src/hooks/__tests__/useIdle.test.ts b/app/src/local-resources/dom-utils/hooks/__tests__/useScreenIdle.test.ts similarity index 62% rename from components/src/hooks/__tests__/useIdle.test.ts rename to app/src/local-resources/dom-utils/hooks/__tests__/useScreenIdle.test.ts index 8063c317325..e449b0cd82e 100644 --- a/components/src/hooks/__tests__/useIdle.test.ts +++ b/app/src/local-resources/dom-utils/hooks/__tests__/useScreenIdle.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { renderHook } from '@testing-library/react' -import { useIdle } from '../useIdle' +import { useScreenIdle } from '../useScreenIdle' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' const MOCK_EVENTS: Array = [ 'mousedown', @@ -20,19 +21,19 @@ describe('useIdle', () => { it('should return the default initialState', () => { const mockTime = 1000 - const { result } = renderHook(() => useIdle(mockTime)) + const { result } = renderHook(() => useScreenIdle(mockTime)) expect(result.current).toBe(true) }) it('should return the given initialState', () => { const mockTime = 1000 - const { result } = renderHook(() => useIdle(mockTime, MOCK_OPTIONS)) + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) expect(result.current).toBe(false) }) it('should return true after 1000ms', () => { const mockTime = 1000 - const { result } = renderHook(() => useIdle(mockTime, MOCK_OPTIONS)) + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) expect(result.current).toBe(false) setTimeout(() => { expect(result.current).toBe(true) @@ -41,7 +42,7 @@ describe('useIdle', () => { it('should return true after 180,000ms - 3min', () => { const mockTime = 60 * 1000 * 3 - const { result } = renderHook(() => useIdle(mockTime, MOCK_OPTIONS)) + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) expect(result.current).toBe(false) setTimeout(() => { expect(result.current).toBe(true) @@ -50,7 +51,7 @@ describe('useIdle', () => { it('should return true after 180,0000ms - 30min', () => { const mockTime = 60 * 1000 * 30 - const { result } = renderHook(() => useIdle(mockTime, MOCK_OPTIONS)) + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) expect(result.current).toBe(false) setTimeout(() => { expect(result.current).toBe(true) @@ -59,10 +60,19 @@ describe('useIdle', () => { it('should return true after 3,600,000ms - 1 hour', () => { const mockTime = 60 * 1000 * 60 - const { result } = renderHook(() => useIdle(mockTime, MOCK_OPTIONS)) + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) expect(result.current).toBe(false) setTimeout(() => { expect(result.current).toBe(true) }, 3600001) }) + + it(`should always return false if the idle time is exactly ${SLEEP_NEVER_MS}`, () => { + const mockTime = SLEEP_NEVER_MS + const { result } = renderHook(() => useScreenIdle(mockTime, MOCK_OPTIONS)) + expect(result.current).toBe(false) + setTimeout(() => { + expect(result.current).toBe(false) + }, 604800001) + }) }) diff --git a/app/src/local-resources/dom-utils/hooks/index.ts b/app/src/local-resources/dom-utils/hooks/index.ts index 2098c90e0c3..97edf1a5b1d 100644 --- a/app/src/local-resources/dom-utils/hooks/index.ts +++ b/app/src/local-resources/dom-utils/hooks/index.ts @@ -1 +1,2 @@ export * from './useScrollPosition' +export * from './useScreenIdle' diff --git a/components/src/hooks/useIdle.ts b/app/src/local-resources/dom-utils/hooks/useScreenIdle.ts similarity index 82% rename from components/src/hooks/useIdle.ts rename to app/src/local-resources/dom-utils/hooks/useScreenIdle.ts index 7fea9f832ca..d36c6cfb326 100644 --- a/components/src/hooks/useIdle.ts +++ b/app/src/local-resources/dom-utils/hooks/useScreenIdle.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' const USER_EVENTS: Array = [ 'click', @@ -29,7 +30,7 @@ const DEFAULT_OPTIONS = { * @param {object} options (events that the app need to check, initialState: initial state true => idle) * @returns {boolean} */ -export function useIdle( +export function useScreenIdle( idleTime: number, options?: Partial<{ events: Array @@ -48,9 +49,12 @@ export function useIdle( window.clearTimeout(idleTimer.current) } - idleTimer.current = window.setTimeout(() => { - setIdle(true) - }, idleTime) + // See RQA-3813 and associated PR. + if (idleTime !== SLEEP_NEVER_MS) { + idleTimer.current = window.setTimeout(() => { + setIdle(true) + }, idleTime) + } } events.forEach(event => { diff --git a/app/src/local-resources/dom-utils/index.ts b/app/src/local-resources/dom-utils/index.ts index fc78d35129c..907d7e254c1 100644 --- a/app/src/local-resources/dom-utils/index.ts +++ b/app/src/local-resources/dom-utils/index.ts @@ -1 +1,2 @@ export * from './hooks' +export * from './constants' diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx index 6533828d803..2d35ca9925c 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx @@ -9,11 +9,11 @@ import { DIRECTION_ROW, Flex, Icon, + LegacyStyledText, Link, + Modal, PrimaryButton, SPACING, - Modal, - LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' import { getTopPortalEl } from '/app/App/portal' diff --git a/app/src/organisms/EmergencyStop/DesktopEstopMissingModal.stories.tsx b/app/src/organisms/EmergencyStop/DesktopEstopMissingModal.stories.tsx index f8729d48263..e089b8cee20 100644 --- a/app/src/organisms/EmergencyStop/DesktopEstopMissingModal.stories.tsx +++ b/app/src/organisms/EmergencyStop/DesktopEstopMissingModal.stories.tsx @@ -2,7 +2,7 @@ import type * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' -import { configReducer } from '/app/redux/config/reducer' +import { configReducer } from '../../redux/config/reducer' import { EstopMissingModal } from '.' import type { Store, StoreEnhancer } from 'redux' diff --git a/app/src/organisms/EmergencyStop/DesktopEstopPressedModal.stories.tsx b/app/src/organisms/EmergencyStop/DesktopEstopPressedModal.stories.tsx index 5b999ce2bc5..917e92143c6 100644 --- a/app/src/organisms/EmergencyStop/DesktopEstopPressedModal.stories.tsx +++ b/app/src/organisms/EmergencyStop/DesktopEstopPressedModal.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux' import { createStore } from 'redux' import { QueryClient, QueryClientProvider } from 'react-query' -import { configReducer } from '/app/redux/config/reducer' +import { configReducer } from '../../redux/config/reducer' import { EstopPressedModal } from '.' import type { Store, StoreEnhancer } from 'redux' diff --git a/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx b/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx index 86ae69e4107..09a144a84a2 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx @@ -76,8 +76,11 @@ export function ProtocolInstrumentMountItem( [] ) const [flowType, setFlowType] = useState(FLOWS.ATTACH) - const selectedPipette = - speccedName === 'p1000_96' ? NINETY_SIX_CHANNEL : SINGLE_MOUNT_PIPETTES + const is96ChannelPipette = + speccedName === 'p1000_96' || speccedName === 'p200_96' + const selectedPipette = is96ChannelPipette + ? NINETY_SIX_CHANNEL + : SINGLE_MOUNT_PIPETTES const handleCalibrate: MouseEventHandler = () => { setFlowType(FLOWS.CALIBRATE) @@ -95,7 +98,7 @@ export function ProtocolInstrumentMountItem( setShowPipetteWizardFlow(true) } } - const is96ChannelPipette = speccedName === 'p1000_96' + const isAttachedWithCal = attachedInstrument != null && attachedInstrument.ok && diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx index cb123b261be..3872d79ba21 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx @@ -14,7 +14,7 @@ import { getOnDeviceDisplaySettings, updateConfigValue, } from '/app/redux/config' -import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' diff --git a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx index a69e095a674..88143b83a69 100644 --- a/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/PipetteWizardFlows/BeforeBeginning.tsx @@ -148,7 +148,10 @@ export const BeforeBeginning = ( const displayName = pipetteDisplayName ?? requiredPipette.pipetteName bodyTranslationKey = 'remove_labware' - if (requiredPipette.pipetteName === 'p1000_96') { + if ( + requiredPipette.pipetteName === 'p1000_96' || + requiredPipette.pipetteName === 'p200_96' + ) { equipmentList = [ { ...NINETY_SIX_CHANNEL_PIPETTE, displayName }, CALIBRATION_PROBE, diff --git a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx index 9d83f3d3e75..b026c40484c 100644 --- a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx @@ -86,7 +86,8 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { } const memoizedAttachedPipettes = useMemo(() => attachedPipettes, []) const is96ChannelPipette = - memoizedAttachedPipettes[mount]?.instrumentName === 'p1000_96' + memoizedAttachedPipettes[mount]?.instrumentName === 'p1000_96' || + memoizedAttachedPipettes[mount]?.instrumentName === 'p200_96' const pipetteName = attachedPipettes[mount] != null ? attachedPipettes[mount]?.displayName diff --git a/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts b/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts index 296c428bc75..f0c40e3b408 100644 --- a/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts +++ b/app/src/organisms/PipetteWizardFlows/getPipetteWizardStepsForProtocol.ts @@ -413,8 +413,11 @@ export const getPipetteWizardStepsForProtocol = ( ): PipetteWizardStep[] | null => { const requiredPipette = pipetteInfo.find(pipette => pipette.mount === mount) const ninetySixChannelAttached = - attachedPipettes[LEFT]?.instrumentName === 'p1000_96' - const ninetySixChannelRequested = requiredPipette?.pipetteName === 'p1000_96' + attachedPipettes[LEFT]?.instrumentName === 'p1000_96' || + attachedPipettes[LEFT]?.instrumentName === 'p200_96' + const ninetySixChannelRequested = + requiredPipette?.pipetteName === 'p1000_96' || + requiredPipette?.pipetteName === 'p200_96' if (requiredPipette == null) { // return empty array if no pipette is required in the protocol diff --git a/app/src/organisms/PipetteWizardFlows/hooks.tsx b/app/src/organisms/PipetteWizardFlows/hooks.tsx index 4d07586c9de..00b7ede1dd9 100644 --- a/app/src/organisms/PipetteWizardFlows/hooks.tsx +++ b/app/src/organisms/PipetteWizardFlows/hooks.tsx @@ -90,10 +90,14 @@ export function usePipetteFlowWizardHeaderText( ) } else if ( attachedPipettes[LEFT]?.data.channels === 96 && - mountPipette?.pipetteName !== 'p1000_96' + mountPipette?.pipetteName !== 'p1000_96' && + mountPipette?.pipetteName !== 'p200_96' ) { return t('detach_96_attach_mount', { mount: capitalizedMount }) - } else if (leftPipette?.pipetteName === 'p1000_96') { + } else if ( + leftPipette?.pipetteName === 'p1000_96' || + leftPipette?.pipetteName === 'p200_96' + ) { if (isGantryEmpty) { return t('attach_96_channel') } else if ( diff --git a/app/src/redux/config/selectors.ts b/app/src/redux/config/selectors.ts index dea9cc3f171..cdbe9bb22c6 100644 --- a/app/src/redux/config/selectors.ts +++ b/app/src/redux/config/selectors.ts @@ -1,5 +1,5 @@ import { createSelector } from 'reselect' -import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import { SLEEP_NEVER_MS } from '/app/local-resources/dom-utils' import type { State } from '../types' import type { Config, diff --git a/components/src/atoms/buttons/LargeButton.tsx b/components/src/atoms/buttons/LargeButton.tsx index 7bfcf4b6533..cea665221fe 100644 --- a/components/src/atoms/buttons/LargeButton.tsx +++ b/components/src/atoms/buttons/LargeButton.tsx @@ -217,14 +217,14 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; background-color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType] .disabledBackgroundColor}; - border: none; + border: 4px solid ${COLORS.grey35}; } &[aria-disabled='true'] { color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledColor}; background-color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType] .disabledBackgroundColor}; - border: none; + border: 4px solid ${COLORS.grey35}; } @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -311,8 +311,8 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { } const ICON_STYLE = css` - width: 1.5rem; - height: 1.5rem; + width: 1.25rem; + height: 1.25rem; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { width: 5rem; height: 5rem; diff --git a/components/src/hooks/index.ts b/components/src/hooks/index.ts index 6ae0015d1e0..14a4de52c71 100644 --- a/components/src/hooks/index.ts +++ b/components/src/hooks/index.ts @@ -2,7 +2,6 @@ export * from './useConditionalConfirm' export * from './useDrag' -export * from './useIdle' export * from './useInterval' export * from './useLongPress' export * from './useMountEffect' diff --git a/components/src/modals/Modal.stories.tsx b/components/src/modals/Modal.stories.tsx index 6adfcbc3274..76cef153d47 100644 --- a/components/src/modals/Modal.stories.tsx +++ b/components/src/modals/Modal.stories.tsx @@ -1,52 +1,36 @@ -import type * as React from 'react' +import { PrimaryButton, StyledText } from '../atoms' +import { SPACING } from '../ui-style-constants' +import { Flex } from '../primitives' +import { JUSTIFY_END } from '../styles' +import { Modal as ModalComponent } from './Modal' -import { LegacyStyledText } from '../atoms' -import { SPACING, TYPOGRAPHY } from '../ui-style-constants' -import { PrimaryBtn } from '../primitives' -import { COLORS } from '../helix-design-system' -import { Modal } from './Modal' +import type { Meta, StoryObj } from '@storybook/react' -import type { Story, Meta } from '@storybook/react' - -export default { +const meta: Meta = { title: 'Library/Molecules/modals/Modal', - component: Modal, -} as Meta - -const Template: Story> = args => ( - -) + component: ModalComponent, +} +export default meta +type Story = StoryObj +const bodyText = 'Modal body goes here' const Children = ( - <> - - {'Modal body goes here'} - + {bodyText} +) - - - {'btn text'} - - - +const Footer = ( + + {}}>{'btn text'} + ) -export const Primary = Template.bind({}) -Primary.args = { - type: 'info', - onClose: () => {}, - closeOnOutsideClick: false, - title: 'Modal Title', - children: Children, +export const Modal: Story = { + args: { + type: 'info', + onClose: () => {}, + closeOnOutsideClick: false, + title: 'Modal Title', + children: Children, + footer: Footer, + }, } diff --git a/components/src/modals/Modal.tsx b/components/src/modals/Modal.tsx index 304b733de8a..7c10694981c 100644 --- a/components/src/modals/Modal.tsx +++ b/components/src/modals/Modal.tsx @@ -66,7 +66,6 @@ export const Modal = (props: ModalProps): JSX.Element => { name: 'ot-alert', color: iconColor(type), size: '1.25rem', - marginRight: SPACING.spacing8, } const modalHeader = ( diff --git a/components/src/modals/ModalHeader.tsx b/components/src/modals/ModalHeader.tsx index b505b1f1a47..6472157400f 100644 --- a/components/src/modals/ModalHeader.tsx +++ b/components/src/modals/ModalHeader.tsx @@ -1,10 +1,15 @@ -import { css } from 'styled-components' +import styled, { css } from 'styled-components' import { Icon } from '../icons' import { Box, Btn, Flex } from '../primitives' -import { LegacyStyledText } from '../atoms' -import { ALIGN_CENTER, JUSTIFY_CENTER, JUSTIFY_SPACE_BETWEEN } from '../styles' -import { SPACING, TYPOGRAPHY } from '../ui-style-constants' +import { StyledText } from '../atoms' +import { + ALIGN_CENTER, + DISPLAY_FLEX, + JUSTIFY_CENTER, + JUSTIFY_SPACE_BETWEEN, +} from '../styles' +import { SPACING } from '../ui-style-constants' import { COLORS } from '../helix-design-system' import type { MouseEventHandler, ReactNode } from 'react' @@ -21,22 +26,6 @@ export interface ModalHeaderProps { closeButton?: ReactNode } -const closeIconStyles = css` - display: flex; - justify-content: ${JUSTIFY_CENTER}; - align-items: ${ALIGN_CENTER}; - border-radius: 0.875rem; - width: 1.625rem; - height: 1.625rem; - &:hover { - background-color: ${COLORS.grey30}; - } - - &:active { - background-color: ${COLORS.grey35}; - } -` - export const ModalHeader = (props: ModalHeaderProps): JSX.Element => { const { icon, @@ -45,57 +34,72 @@ export const ModalHeader = (props: ModalHeaderProps): JSX.Element => { titleElement1, titleElement2, backgroundColor, - color, + color = COLORS.black90, closeButton, } = props return ( <> - - + {icon != null && } {titleElement1} {titleElement2} - {/* TODO (nd: 08/07/2024) Convert to StyledText once designs are resolved */} - + {title} - + - {closeButton != null - ? closeButton - : onClose != null && ( - - - - )} - - + {closeButton != null || + (onClose != null && ( + + + + ))} + + ) } + +const StyledModalHeader = styled(Flex)` + padding: ${SPACING.spacing16} ${SPACING.spacing24}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + align-items: ${ALIGN_CENTER}; + background-color: ${props => props.backgroundColor}; +` + +const StyledDivider = styled(Box)` + border-bottom: 1px solid ${COLORS.grey30}; + margin: 0; + width: 100%; +` + +const closeIconStyles = css` + display: ${DISPLAY_FLEX}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + border-radius: 0.875rem; + width: 1.625rem; + height: 1.625rem; + &:hover { + background-color: ${COLORS.grey30}; + } + + &:active { + background-color: ${COLORS.grey35}; + } +` diff --git a/components/src/modals/ModalShell.tsx b/components/src/modals/ModalShell.tsx index c6fa7e5ccb3..82ae09b8868 100644 --- a/components/src/modals/ModalShell.tsx +++ b/components/src/modals/ModalShell.tsx @@ -3,6 +3,7 @@ import { ALIGN_CENTER, ALIGN_END, CURSOR_DEFAULT, + DISPLAY_FLEX, JUSTIFY_CENTER, JUSTIFY_END, OVERFLOW_AUTO, @@ -110,7 +111,7 @@ const ContentArea = styled.div<{ position: Position noPadding: boolean }>` - display: flex; + display: ${DISPLAY_FLEX}; position: ${POSITION_ABSOLUTE}; align-items: ${({ position }) => position === 'center' ? ALIGN_CENTER : ALIGN_END}; @@ -137,6 +138,7 @@ const ModalArea = styled.div< box-shadow: ${BORDERS.smallDropShadow}; height: ${({ isFullPage }) => (isFullPage ? '100%' : 'auto')}; background-color: ${COLORS.white}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border-radius: ${BORDERS.borderRadius16}; } diff --git a/components/src/modals/__tests__/ModalHeader.test.tsx b/components/src/modals/__tests__/ModalHeader.test.tsx index bf55110b63f..051dfea623d 100644 --- a/components/src/modals/__tests__/ModalHeader.test.tsx +++ b/components/src/modals/__tests__/ModalHeader.test.tsx @@ -3,10 +3,9 @@ import '@testing-library/jest-dom/vitest' import { describe, it, expect, vi, beforeEach } from 'vitest' import { renderWithProviders } from '../../testing/utils' -import { ModalHeader } from '../ModalHeader' import { COLORS } from '../../helix-design-system' -import { SPACING } from '../../ui-style-constants' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' +import { ModalHeader } from '../ModalHeader' import type { ComponentProps } from 'react' @@ -40,7 +39,6 @@ describe('ModalHeader', () => { name: 'ot-alert', color: COLORS.black90, size: '1.25rem', - marginRight: SPACING.spacing8, } render(props) expect(screen.getByTestId('Modal_header_icon')).toHaveStyle( @@ -52,9 +50,6 @@ describe('ModalHeader', () => { expect(screen.getByTestId('Modal_header_icon')).toHaveStyle( `height: 1.25rem` ) - expect(screen.getByTestId('Modal_header_icon')).toHaveStyle( - `margin-right: ${SPACING.spacing8}` - ) }) it('should call a mock function when clicking close icon', () => { diff --git a/components/src/molecules/DeckInfoLabel/index.tsx b/components/src/molecules/DeckInfoLabel/index.tsx index 12fba78b93b..d79652da38f 100644 --- a/components/src/molecules/DeckInfoLabel/index.tsx +++ b/components/src/molecules/DeckInfoLabel/index.tsx @@ -7,19 +7,19 @@ import { Flex } from '../../primitives' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' -import type { ModuleIconName } from '../../icons' +import type { IconName, ModuleIconName } from '../../icons' import type { StyleProps } from '../../primitives' interface DeckLabelProps extends StyleProps { /** deck label to display */ deckLabel: string - iconName?: undefined + iconName?: IconName } interface HardwareIconProps extends StyleProps { /** hardware icon name */ iconName: ModuleIconName | 'stacked' - deckLabel?: undefined + deckLabel?: string } // type union requires one of deckLabel or iconName, but not both diff --git a/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx b/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx index d248ca2a669..7a48299e7dc 100644 --- a/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx +++ b/components/src/molecules/InfoScreen/__tests__/InfoScreen.test.tsx @@ -37,5 +37,21 @@ describe('InfoScreen', () => { expect(screen.getByLabelText('alert')).toHaveStyle( `color: ${COLORS.grey60}` ) + expect(screen.getByTestId('InfoScreen')).toHaveStyle(`height: 100%`) + }) + + it('should render set height, subContent and backgroundColor', () => { + props = { + ...props, + subContent: 'mock sub content', + backgroundColor: COLORS.blue50, + height: '10rem', + } + render(props) + screen.getByText('mock sub content') + expect(screen.getByTestId('InfoScreen')).toHaveStyle( + `background-color: ${COLORS.blue50}` + ) + expect(screen.getByTestId('InfoScreen')).toHaveStyle(`height: 10rem`) }) }) diff --git a/components/src/molecules/InfoScreen/index.tsx b/components/src/molecules/InfoScreen/index.tsx index a70e2c409d7..d92f7cd64e7 100644 --- a/components/src/molecules/InfoScreen/index.tsx +++ b/components/src/molecules/InfoScreen/index.tsx @@ -16,7 +16,7 @@ export function InfoScreen({ content, subContent, backgroundColor = COLORS.grey30, - height, + height = '100%', }: InfoScreenProps): JSX.Element { return ( = { component: DeckLabelSetComponent, decorators: [ Story => ( - + ), @@ -38,8 +41,8 @@ export const DeckLabel: Story = { args: { // width and height from Figma deckLabels: mockDeckLabels, - width: 31.9375, - height: 5.75, + width: 31.9375 * 16, + height: 5.75 * 16, x: 0, y: 0, }, diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 45b50b5a579..a3cdbe28417 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -100,6 +100,7 @@ test-photometric: $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 50 --trials 1 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 200 --trials 1 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 200 --channels 96 --tip 50 --trials 1 + $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 200 --channels 96 --tip 20 --trials 1 .PHONY: test-gravimetric-single test-gravimetric-single: @@ -123,6 +124,7 @@ test-gravimetric-multi: test-gravimetric-96: $(python) -m hardware_testing.gravimetric --simulate --pipette 1000 --channels 96 --trials 2 --no-blank $(python) -m hardware_testing.gravimetric --simulate --pipette 200 --channels 96 --trials 2 --no-blank + $(python) -m hardware_testing.gravimetric --simulate --pipette 200 --channels 96 --trials 1 --no-blank --increment --tip 20 .PHONY: test-gravimetric test-gravimetric: @@ -229,6 +231,11 @@ push-no-restart-ot3: sdist Pipfile.lock .PHONY: push-ot3 push-ot3: push-no-restart-ot3 push-plot-webpage-ot3 push-description-ot3 push-labware-ot3 +.PHONE: open-dev-app +open-dev-app: + cd .. && $(MAKE) -C app dev + + .PHONY: push-all push-all: clean wheel push-no-restart push-plot-webpage-ot3 diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py index f297f0e7e3a..0c32f1f70b4 100644 --- a/hardware-testing/hardware_testing/gravimetric/__main__.py +++ b/hardware-testing/hardware_testing/gravimetric/__main__.py @@ -70,11 +70,12 @@ GRAVIMETRIC_CFG_INCREMENT = { 50: { - 1: {50: gravimetric_ot3_p50_single}, + 1: {20: gravimetric_ot3_p50_single, 50: gravimetric_ot3_p50_single}, 8: {50: gravimetric_ot3_p50_multi_50ul_tip_increment}, }, 200: { 96: { + 20: gravimetric_ot3_p200_96, 50: gravimetric_ot3_p200_96, 200: gravimetric_ot3_p200_96, }, @@ -117,6 +118,7 @@ PHOTOMETRIC_CFG = { 50: { 1: { + 20: photometric_ot3_p50_single, 50: photometric_ot3_p50_single, }, 8: { @@ -124,7 +126,11 @@ }, }, 200: { - 96: {50: photometric_ot3_p200_96, 200: photometric_ot3_p200_96}, + 96: { + 20: photometric_ot3_p200_96, + 50: photometric_ot3_p200_96, + 200: photometric_ot3_p200_96, + }, }, 1000: { 1: { @@ -137,7 +143,11 @@ 200: photometric_ot3_p1000_multi, 1000: photometric_ot3_p1000_multi, }, - 96: {50: photometric_ot3_p1000_96, 200: photometric_ot3_p1000_96}, + 96: { + 20: photometric_ot3_p1000_96, + 50: photometric_ot3_p1000_96, + 200: photometric_ot3_p1000_96, + }, }, } @@ -177,11 +187,11 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: "Starting opentrons-robot-server, so we can http GET labware offsets" ) LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) - ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") - for offset in LABWARE_OFFSETS: - ui.print_info(f"\t{offset.createdAt}:") - ui.print_info(f"\t\t{offset.definitionUri}") - ui.print_info(f"\t\t{offset.vector}") + # ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + # for offset in LABWARE_OFFSETS: + # ui.print_info(f"\t{offset.createdAt}:") + # ui.print_info(f"\t\t{offset.definitionUri}") + # ui.print_info(f"\t\t{offset.vector}") # gather the custom labware (for simulation) custom_defs = {} if args.simulate: @@ -572,7 +582,7 @@ def _main( parser.add_argument("--simulate", action="store_true") parser.add_argument("--pipette", type=int, choices=[50, 200, 1000], required=True) parser.add_argument("--channels", type=int, choices=[1, 8, 96], default=1) - parser.add_argument("--tip", type=int, choices=[0, 50, 200, 1000], default=0) + parser.add_argument("--tip", type=int, choices=[0, 20, 50, 200, 1000], default=0) parser.add_argument("--trials", type=int, default=0) parser.add_argument("--increment", action="store_true") parser.add_argument("--return-tip", action="store_true") diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 968de3ecca7..ef7f989faa4 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -88,30 +88,40 @@ class PhotometricConfig(VolumetricConfig): LIQUID_PROBE_SETTINGS: Dict[int, Dict[int, Dict[int, Dict[str, int]]]] = { 50: { 1: { + 20: { + "mount_speed": 5, + "plunger_speed": 15, + "sensor_threshold_pascals": 15, + }, 50: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, }, 8: { 50: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, }, }, 200: { 96: { + 20: { + "mount_speed": 5, + "plunger_speed": 5, + "sensor_threshold_pascals": 15, + }, 50: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 5, "sensor_threshold_pascals": 15, }, 200: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 5, "sensor_threshold_pascals": 15, }, } @@ -120,51 +130,56 @@ class PhotometricConfig(VolumetricConfig): 1: { 50: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, 200: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, 1000: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, }, 8: { 50: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, 200: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, 1000: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 15, "sensor_threshold_pascals": 15, }, }, 96: { + 20: { + "mount_speed": 5, + "plunger_speed": 5, + "sensor_threshold_pascals": 15, + }, 50: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 5, "sensor_threshold_pascals": 15, }, 200: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 5, "sensor_threshold_pascals": 15, }, 1000: { "mount_speed": 5, - "plunger_speed": 20, + "plunger_speed": 5, "sensor_threshold_pascals": 15, }, }, @@ -194,6 +209,7 @@ def _get_liquid_probe_settings( QC_VOLUMES_G: Dict[int, Dict[int, List[Tuple[int, List[float]]]]] = { 1: { 50: [ # P50 + (20, [1.0, 20.0]), (50, [1.0, 50.0]), # T50 ], 1000: [ # P1000 @@ -214,10 +230,12 @@ def _get_liquid_probe_settings( }, 96: { 200: [ + (20, [0.5, 1.0]), # T20 (50, [1.0, 50.0]), # T50 (200, [200.0]), # T200 ], 1000: [ # P1000 + (20, [5.0]), (50, [5.0]), # T50 (200, [200.0]), # T200 (1000, [1000.0]), # T1000 @@ -279,7 +297,8 @@ def _get_liquid_probe_settings( }, 96: { 200: [ - (50, [1.0, 5.0]), # T50 + (20, [1.0, 0.5]), # T50 + (50, [50.0]), # T50 (200, [200.0]), # T200 ], 1000: [ # P1000 @@ -311,6 +330,11 @@ def _get_liquid_probe_settings( # channels: [Pipette: [tip: [Volume: (%d, Cv)]]] 1: { 50: { # P50 + 20: { + 1.0: (5.0, 4.0), + 10.0: (1.0, 0.5), + 20.0: (1, 0.4), + }, 50: { 1.0: (5.0, 4.0), 10.0: (1.0, 0.5), @@ -363,12 +387,16 @@ def _get_liquid_probe_settings( }, 96: { 200: { - 50: { # T50 + 20: { # T20 + 0.5: (2.5, 2.0), 1.0: (2.5, 2.0), 2.0: (2.5, 2.0), 3.0: (2.5, 2.0), 5.0: (2.5, 2.0), 10.0: (3.1, 1.7), + }, + 50: { # T50 + 1.0: (2.5, 2.0), 50.0: (1.5, 0.75), }, 200: { # T200 @@ -378,6 +406,14 @@ def _get_liquid_probe_settings( }, }, 1000: { # P1000 + 20: { # T20 + 1.0: (2.5, 2.0), + 2.0: (2.5, 2.0), + 3.0: (2.5, 2.0), + 5.0: (2.5, 2.0), + 10.0: (3.1, 1.7), + 20.0: (3.1, 1.7), + }, 50: { # T50 1.0: (2.5, 2.0), 2.0: (2.5, 2.0), diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py index 44752543fd5..118eb164bcb 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute.py +++ b/hardware-testing/hardware_testing/gravimetric/execute.py @@ -591,7 +591,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq assert resources.recorder is not None recorder = resources.recorder if resources.ctx.is_simulating(): - start_sim_mass = {50: 15, 200: 200, 1000: 200} + start_sim_mass = {20: 5, 50: 15, 200: 200, 1000: 200} resources.recorder.set_simulation_mass(start_sim_mass[cfg.tip_volume]) os.makedirs( f"{resources.test_report.parent}/{resources.test_report._run_id}", exist_ok=True diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py index 217109dd89d..a13da2785af 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py +++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py @@ -46,6 +46,7 @@ "B": {"min": 10, "max": 49.99}, "C": {"min": 2, "max": 9.999}, "D": {"min": 1, "max": 1.999}, + "E": {"min": 0, "max": 0.9999}, } _MIN_START_VOLUME_UL = {1: 500, 8: 3000, 96: 30000} _MIN_END_VOLUME_UL = {1: 400, 8: 3000, 96: 10000} @@ -191,6 +192,8 @@ def _record_measurement_and_store(m_type: MeasurementType) -> EnvironmentData: ) _record_measurement_and_store(MeasurementType.ASPIRATE) + if not trial.ctx.is_simulating(): + input("请记录吸液状态,并尝试拍摄清晰的吸液后的针管照片..........") for i in range(num_dispenses): dest_name = _get_photo_plate_dest(trial.cfg, trial.trial) dest_well = trial.dest[dest_name] @@ -215,6 +218,8 @@ def _record_measurement_and_store(m_type: MeasurementType) -> EnvironmentData: touch_tip=trial.cfg.touch_tip, ) _record_measurement_and_store(MeasurementType.DISPENSE) + if not trial.ctx.is_simulating(): + input("请记录排液状态,并尝试拍摄清晰的排液后的针管照片..........") trial.pipette._retract() # retract to top of gantry if (i + 1) == num_dispenses: if not trial.cfg.same_tip: diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index fdeb8fa636e..5b3fcdc7a7e 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -485,6 +485,7 @@ def _load_tipracks( ) for slot in cfg.slots_tiprack ] + print(f"LOAD TIPRack{use_adapters}") for ls in tiprack_load_settings: ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') diff --git a/hardware-testing/hardware_testing/gravimetric/increments.py b/hardware-testing/hardware_testing/gravimetric/increments.py index 76c90a45b44..069e879438c 100644 --- a/hardware-testing/hardware_testing/gravimetric/increments.py +++ b/hardware-testing/hardware_testing/gravimetric/increments.py @@ -267,9 +267,27 @@ }, 96: { 200: { + 20: { + "default": [ + 0.800, + 1.000, + 1.300, + 1.700, + 3.000, + 5.000, + 10.000, + 15.000, + 20.000, + 24.000, + ], + }, 50: { "default": [ + 0.700, + 1.000, + 1.500, 2.000, + 2.500, 3.000, 4.000, 5.000, @@ -283,41 +301,21 @@ 40.000, 60.000, ], - "lowVolumeDefault": [ - 1.100, - 1.200, - 1.370, - 1.700, - 2.040, - 2.660, - 3.470, - 3.960, - 4.350, - 4.800, - 5.160, - 5.890, - 6.730, - 8.200, - 10.020, - 11.100, - 14.910, - 28.940, - 48.27, - ], }, 200: { "default": [ + 1.000, 2.000, - 3.000, 4.000, - 5.000, 6.000, - 7.000, 8.000, - 9.000, 10.000, + 20.000, 50.000, 100.000, + 150.000, + 200.000, + 210.000, 220.000, ], }, diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py index c7021e60585..e9fc477446f 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py @@ -11,6 +11,8 @@ _default_submerge_aspirate_mm = 1.5 _p50_multi_submerge_aspirate_mm = 1.5 _default_submerge_dispense_mm = 1.5 +_p200_default_submerge_aspirate_mm = 2.5 +_p200_default_submerge_dispense_mm = 3.0 _default_retract_mm = 5.0 _default_retract_discontinuity = 20 @@ -25,6 +27,38 @@ _dispense_defaults: Dict[int, Dict[int, Dict[int, Dict[int, DispenseSettings]]]] = { 1: { 50: { # P50 + 20: { # T20 + 1: DispenseSettings( # 1uL + z_submerge_depth=_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_p50_ul_sec_sec, + plunger_flow_rate=57, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=7, + blow_out_flow_rate=57, + ), + 10: DispenseSettings( # 10uL + z_submerge_depth=_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_p50_ul_sec_sec, + plunger_flow_rate=57, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=2, + blow_out_flow_rate=57, + ), + 20: DispenseSettings( # 20uL + z_submerge_depth=_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_p50_ul_sec_sec, + plunger_flow_rate=57, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=2, + blow_out_flow_rate=57, + ), + }, 50: { # T50 1: DispenseSettings( # 1uL z_submerge_depth=_default_submerge_dispense_mm, @@ -34,6 +68,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=7, + blow_out_flow_rate=57, ), 10: DispenseSettings( # 10uL z_submerge_depth=_default_submerge_dispense_mm, @@ -43,6 +78,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=2, + blow_out_flow_rate=57, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -52,6 +88,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=2, + blow_out_flow_rate=57, ), }, }, @@ -65,6 +102,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=318, ), 10: DispenseSettings( # 10uL z_submerge_depth=_default_submerge_dispense_mm, @@ -74,6 +112,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=418, ), 50: DispenseSettings( # 10uL z_submerge_depth=_default_submerge_dispense_mm, @@ -83,6 +122,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=418, ), }, 200: { # T200 @@ -94,6 +134,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=716, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -103,6 +144,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=716, ), 200: DispenseSettings( # 200uL z_submerge_depth=_default_submerge_dispense_mm, @@ -112,6 +154,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=716, ), }, 1000: { # T1000 @@ -123,6 +166,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=160, ), 100: DispenseSettings( # 100uL z_submerge_depth=_default_submerge_dispense_mm, @@ -132,6 +176,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=716, ), 1000: DispenseSettings( # 1000uL z_submerge_depth=_default_submerge_dispense_mm, @@ -141,6 +186,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=716, ), }, }, @@ -156,6 +202,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=6, + blow_out_flow_rate=57, ), 10: DispenseSettings( # 5uL z_submerge_depth=_default_submerge_dispense_mm, @@ -165,6 +212,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=2, + blow_out_flow_rate=57, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -174,6 +222,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=2, + blow_out_flow_rate=57, ), }, }, @@ -187,6 +236,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=318, ), 10: DispenseSettings( # 10uL z_submerge_depth=_default_submerge_dispense_mm, @@ -196,6 +246,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=478, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -205,6 +256,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=478, ), }, 200: { # T200 @@ -216,6 +268,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=716, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -225,6 +278,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=716, ), 200: DispenseSettings( # 200uL z_submerge_depth=_default_submerge_dispense_mm, @@ -234,6 +288,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=716, ), }, 1000: { # T1000 @@ -245,6 +300,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=160, ), 100: DispenseSettings( # 100uL z_submerge_depth=_default_submerge_dispense_mm, @@ -254,6 +310,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=716, ), 1000: DispenseSettings( # 1000uL z_submerge_depth=_default_submerge_dispense_mm, @@ -263,12 +320,45 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=716, ), }, }, }, 96: { 1000: { # P1000 + 20: { # T20 + 1: DispenseSettings( # 5uL + z_submerge_depth=_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=80, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=5, + blow_out_flow_rate=80, + ), + 5: DispenseSettings( # 10uL + z_submerge_depth=_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=80, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=5, + blow_out_flow_rate=80, + ), + 20: DispenseSettings( # 50uL + z_submerge_depth=_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=80, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=5, + blow_out_flow_rate=80, + ), + }, 50: { # T50 5: DispenseSettings( # 5uL z_submerge_depth=_default_submerge_dispense_mm, @@ -278,6 +368,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=80, ), 10: DispenseSettings( # 10uL z_submerge_depth=_default_submerge_dispense_mm, @@ -287,6 +378,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=80, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -296,6 +388,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=80, ), }, 200: { # T200 @@ -307,6 +400,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=80, ), 50: DispenseSettings( # 50uL z_submerge_depth=_default_submerge_dispense_mm, @@ -316,6 +410,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=80, ), 200: DispenseSettings( # 200uL z_submerge_depth=_default_submerge_dispense_mm, @@ -325,6 +420,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=80, ), }, 1000: { # T1000 @@ -336,6 +432,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=80, ), 100: DispenseSettings( # 100uL z_submerge_depth=_default_submerge_dispense_mm, @@ -345,6 +442,7 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=80, ), 1000: DispenseSettings( # 1000uL z_submerge_depth=_default_submerge_dispense_mm, @@ -354,66 +452,105 @@ z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=20, + blow_out_flow_rate=80, ), }, }, 200: { # P200 + 20: { # T20 + 1: DispenseSettings( # 5uL + z_submerge_depth=_p200_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=3.5, + blow_out_flow_rate=22, + ), + 5: DispenseSettings( # 10uL + z_submerge_depth=_p200_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=7, + blow_out_flow_rate=5, + ), + 20: DispenseSettings( # 20uL + z_submerge_depth=_p200_default_submerge_dispense_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_dispense_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + blow_out_submerged=7, + blow_out_flow_rate=5, + ), + }, 50: { # T50 5: DispenseSettings( # 5uL - z_submerge_depth=_default_submerge_dispense_mm, + z_submerge_depth=_p200_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=6.5, # ul/sec delay=_default_dispense_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=10, ), 10: DispenseSettings( # 10uL - z_submerge_depth=_default_submerge_dispense_mm, + z_submerge_depth=_p200_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=6.5, # ul/sec delay=_default_dispense_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=10, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_default_submerge_dispense_mm, + z_submerge_depth=_p200_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=6.5, # ul/sec delay=_default_dispense_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, blow_out_submerged=5, + blow_out_flow_rate=10, ), }, 200: { # T200 5: DispenseSettings( # 5uL - z_submerge_depth=_default_submerge_dispense_mm, + z_submerge_depth=_p200_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=15, # ul/sec delay=_default_dispense_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, - blow_out_submerged=5, + blow_out_submerged=15, + blow_out_flow_rate=10, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_default_submerge_dispense_mm, + z_submerge_depth=_p200_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=15, # ul/sec delay=_default_dispense_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, - blow_out_submerged=5, + blow_out_submerged=15, + blow_out_flow_rate=10, ), 200: DispenseSettings( # 200uL - z_submerge_depth=_default_submerge_dispense_mm, + z_submerge_depth=_p200_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=15, # ul/sec delay=_default_dispense_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, - blow_out_submerged=5, + blow_out_submerged=15, + blow_out_flow_rate=10, ), }, }, @@ -423,6 +560,38 @@ _aspirate_defaults: Dict[int, Dict[int, Dict[int, Dict[int, AspirateSettings]]]] = { 1: { 50: { # P50 + 20: { # T20 + 1: AspirateSettings( # 1uL + z_submerge_depth=_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_p50_ul_sec_sec, + plunger_flow_rate=35, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + 10: AspirateSettings( # 10uL + z_submerge_depth=_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_p50_ul_sec_sec, + plunger_flow_rate=23.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + 20: AspirateSettings( # 20uL + z_submerge_depth=_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_p50_ul_sec_sec, + plunger_flow_rate=35, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + }, 50: { # T50 1: AspirateSettings( # 1uL z_submerge_depth=_default_submerge_aspirate_mm, @@ -691,6 +860,38 @@ }, 96: { 1000: { # P1000 + 20: { # T20 + 1: AspirateSettings( # 5uL + z_submerge_depth=_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + 5: AspirateSettings( # 10uL + z_submerge_depth=_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + 20: AspirateSettings( # 50uL + z_submerge_depth=_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + }, 50: { # T50 5: AspirateSettings( # 5uL z_submerge_depth=_default_submerge_aspirate_mm, @@ -789,9 +990,41 @@ }, }, 200: { # P200 + 20: { # T20 + 1: AspirateSettings( # 5uL + z_submerge_depth=_p200_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + 5: AspirateSettings( # 10uL + z_submerge_depth=_p200_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + 20: AspirateSettings( # 20uL + z_submerge_depth=_p200_default_submerge_aspirate_mm, + plunger_acceleration=_default_accel_96ch_ul_sec_sec, + plunger_flow_rate=6.5, # ul/sec + delay=_default_aspirate_delay_seconds, + z_retract_discontinuity=_default_retract_discontinuity, + z_retract_height=_default_retract_mm, + leading_air_gap=0, + trailing_air_gap=0.1, + ), + }, 50: { # T50 5: AspirateSettings( # 5uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p200_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -801,7 +1034,7 @@ trailing_air_gap=0.1, ), 10: AspirateSettings( # 10uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p200_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -811,7 +1044,7 @@ trailing_air_gap=0.1, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p200_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -823,9 +1056,9 @@ }, 200: { # T200 5: AspirateSettings( # 5uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p200_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=15, # ul/sec delay=_default_aspirate_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, @@ -833,9 +1066,9 @@ trailing_air_gap=2, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p200_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=15, # ul/sec delay=_default_aspirate_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, @@ -843,9 +1076,9 @@ trailing_air_gap=3.5, ), 200: AspirateSettings( # 200uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p200_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, - plunger_flow_rate=80, # ul/sec + plunger_flow_rate=15, # ul/sec delay=_default_aspirate_delay_seconds, z_retract_discontinuity=_default_retract_discontinuity, z_retract_height=_default_retract_mm, diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/definition.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/definition.py index 229d16c4b1a..9f03efe759a 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/definition.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/definition.py @@ -27,6 +27,7 @@ class DispenseSettings(LiquidSettings): """Dispense Settings.""" blow_out_submerged: float # microliters + blow_out_flow_rate: float # ul/s @dataclass @@ -90,5 +91,8 @@ def _interp(lower: float, upper: float) -> float: blow_out_submerged=_interp( a.dispense.blow_out_submerged, b.dispense.blow_out_submerged ), + blow_out_flow_rate=_interp( + a.dispense.blow_out_flow_rate, b.dispense.blow_out_flow_rate + ), ), ) diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py index 9f059559f13..81fb1180580 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py @@ -313,7 +313,7 @@ def _dispense_on_retract() -> None: # PHASE 1: APPROACH pipette.flow_rate.aspirate = liquid_class.aspirate.plunger_flow_rate pipette.flow_rate.dispense = liquid_class.dispense.plunger_flow_rate - pipette.flow_rate.blow_out = liquid_class.dispense.plunger_flow_rate + pipette.flow_rate.blow_out = liquid_class.dispense.blow_out_flow_rate pipette.move_to(well.bottom(approach_mm).move(channel_offset)) _aspirate_on_approach() if aspirate or mix else _dispense_on_approach() @@ -388,10 +388,20 @@ def aspirate_with_liquid_class( clear_accuracy_function: bool = False, ) -> None: """Aspirate with liquid class.""" - pip_size = 50 if "50" in pipette.name else 1000 + if "50" in pipette.name: + pip_size = 50 + elif "200" in pipette.name: + pip_size = 200 + else: + pip_size = 1000 + print(f"pip_size:{pip_size}") + print(f"pipette channels :{pipette.channels}") + print(f"tip volume :{tip_volume}") + # pip_size = 50 if "50" in pipette.name else 1000 liquid_class = get_liquid_class( pip_size, pipette.channels, tip_volume, int(aspirate_volume) ) + print(f"aspirate liquid class : {(liquid_class.aspirate)}") _pipette_with_liquid_settings( ctx, pipette, @@ -426,10 +436,20 @@ def dispense_with_liquid_class( clear_accuracy_function: bool = False, ) -> None: """Dispense with liquid class.""" - pip_size = 50 if "50" in pipette.name else 1000 + if "50" in pipette.name: + pip_size = 50 + elif "200" in pipette.name: + pip_size = 200 + else: + pip_size = 1000 + print(f"pip_size:{pip_size}") + print(f"pipette channels :{pipette.channels}") + print(f"tip volume :{tip_volume}") + # pip_size = 50 if "50" in pipette.name else 1000 liquid_class = get_liquid_class( pip_size, pipette.channels, tip_volume, int(dispense_volume) ) + print(f"dispense liquid class : {(liquid_class.dispense)}") _pipette_with_liquid_settings( ctx, pipette, diff --git a/hardware-testing/hardware_testing/gravimetric/report.py b/hardware-testing/hardware_testing/gravimetric/report.py index 3a15e0b213e..94b80978e1d 100644 --- a/hardware-testing/hardware_testing/gravimetric/report.py +++ b/hardware-testing/hardware_testing/gravimetric/report.py @@ -104,6 +104,7 @@ def create_csv_test_report_photometric( lines=[ CSVLine("robot", [str]), CSVLine("pipette", [str]), + CSVLine("tips_20ul", [str]), CSVLine("tips_50ul", [str]), CSVLine("tips_200ul", [str]), CSVLine("tips_1000ul", [str]), @@ -205,6 +206,7 @@ def _field_type_not_using_typing(t: Any) -> Any: lines=[ CSVLine("robot", [str]), CSVLine("pipette", [str]), + CSVLine("tips_20ul", [str]), CSVLine("tips_50ul", [str]), CSVLine("tips_200ul", [str]), CSVLine("tips_1000ul", [str]), diff --git a/hardware-testing/hardware_testing/gravimetric/workarounds.py b/hardware-testing/hardware_testing/gravimetric/workarounds.py index 7c182ddd079..5bb00de5575 100644 --- a/hardware-testing/hardware_testing/gravimetric/workarounds.py +++ b/hardware-testing/hardware_testing/gravimetric/workarounds.py @@ -4,7 +4,6 @@ from typing import List import platform from json import loads as json_loads -from hardware_testing.data import ui from opentrons.hardware_control import SyncHardwareAPI from opentrons.protocol_api.labware import Labware from opentrons.protocol_api import InstrumentContext, ProtocolContext @@ -96,7 +95,7 @@ def _offset_applies_to_labware(_o: dict) -> bool: return False offset_uri = _o["definitionUri"] if offset_uri[0:-1] != lw_uri[0:-1]: # drop schema version number - ui.print_info(f"{_o} does not apply {offset_uri} != {lw_uri}") + # ui.print_info(f"{_o} does not apply {offset_uri} != {lw_uri}") # NOTE: we're allowing tip-rack adapters to share offsets # because it doesn't make a difference which volume # of tip it holds diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index 084f99eadee..6d3bf8e6031 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -38,6 +38,7 @@ from hardware_testing.protocols.liquid_sense_lpc import ( liquid_sense_ot3_p50_single_vial, liquid_sense_ot3_p50_multi_vial, + liquid_sense_ot3_p200_96_vial, liquid_sense_ot3_p1000_96_vial, liquid_sense_ot3_p1000_single_vial, liquid_sense_ot3_p1000_multi_vial, @@ -53,7 +54,7 @@ CREDENTIALS_PATH = "/var/lib/jupyter/notebooks/abr.json" -API_LEVEL = "2.18" +API_LEVEL = "2.21" LABWARE_OFFSETS: List[LabwareOffset] = [] @@ -75,6 +76,9 @@ 1: liquid_sense_ot3_p50_single_vial, 8: liquid_sense_ot3_p50_multi_vial, }, + 200: { + 96: liquid_sense_ot3_p200_96_vial, + }, 1000: { 1: liquid_sense_ot3_p1000_single_vial, 8: liquid_sense_ot3_p1000_multi_vial, @@ -92,6 +96,7 @@ 8: "p1000_multi_flex", 96: "p1000_96_flex", }, + 200: {96: "p200_96_flex"}, } @@ -188,8 +193,10 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": if args.tip == 0: if args.pipette == 1000: tip_volumes: List[int] = [50, 200, 1000] - else: + elif args.pipette == 50: tip_volumes = [50] + else: + tip_volumes = [20, 50, 200] else: tip_volumes = [args.tip] @@ -265,22 +272,24 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": parser = argparse.ArgumentParser("Pipette Testing") parser.add_argument("--simulate", action="store_true") - parser.add_argument("--pipette", type=int, choices=[50, 1000], required=True) + parser.add_argument("--pipette", type=int, choices=[50, 200, 1000], required=True) parser.add_argument("--mount", type=str, choices=["left", "right"], default="left") parser.add_argument("--channels", type=int, choices=[1, 8, 96], default=1) - parser.add_argument("--tip", type=int, choices=[0, 50, 200, 1000], default=0) + parser.add_argument("--tip", type=int, choices=[0, 20, 50, 200, 1000], default=0) parser.add_argument("--return-tip", action="store_true") - parser.add_argument("--trials", type=int, default=7) - parser.add_argument("--trials-before-jog", type=int, default=7) + parser.add_argument("--trials", type=int, default=10) + parser.add_argument("--trials-before-jog", type=int, default=10) parser.add_argument("--z-speed", type=float, default=5) parser.add_argument("--aspirate", action="store_true") - parser.add_argument("--plunger-speed", type=float, default=15) + parser.add_argument("--plunger-speed", type=float, default=5) parser.add_argument("--no-multi-pass", action="store_true") parser.add_argument("--wet", action="store_true") parser.add_argument("--starting-tip", type=str, default="A1") parser.add_argument("--test-well", type=str, default="A1") parser.add_argument("--p-solo-time", type=float, default=0) - parser.add_argument("--google-sheet-name", type=str, default="LLD-Shared-Data") + parser.add_argument( + "--google-sheet-name", type=str, default="LLD-Shared-Data-96ch-200ul" + ) parser.add_argument( "--gd-parent-folder", type=str, default="1b2V85fDPA0tNqjEhyHOGCWRZYgn8KsGf" ) diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 001abdaa82f..76f5a9f2799 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -15,9 +15,11 @@ from hardware_testing.data import ui, get_testing_data_directory from opentrons.hardware_control.types import ( InstrumentProbeType, + PipetteSensorId, OT3Mount, Axis, top_types, + PipetteSensorResponseQueue, ) from opentrons.hardware_control.dev_types import PipetteDict @@ -420,6 +422,7 @@ def _run_trial( probes.append(InstrumentProbeType.SECONDARY) probe_target = InstrumentProbeType.BOTH data_files: Dict[InstrumentProbeType, str] = {} + data_capture: PipetteSensorResponseQueue = PipetteSensorResponseQueue() for probe in probes: data_filename = f"pressure_sensor_data-trial{trial}-tip{tip}-{probe.name}.csv" data_file = f"{data_dir}/{run_args.name}/{run_args.run_id}/{data_filename}" @@ -456,8 +459,42 @@ def _run_trial( run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") # TODO add in stuff for secondary probe try: - height = hw_api.liquid_probe(hw_mount, z_distance, lps, probe_target) + height = hw_api.liquid_probe( + hw_mount, z_distance, lps, probe_target, response_queue=data_capture + ) result: LLDResult = LLDResult.success + # write the data files that used to be made automatically + if not run_args.ctx.is_simulating(): + for probe in probes: + sensor_id = ( + PipetteSensorId.S0 + if probe == InstrumentProbeType.PRIMARY + else PipetteSensorId.S1 + ) + as_dict = data_capture.get_nowait() + data = [d.to_float() for d in as_dict[sensor_id]] + with open(data_files[probe], "w") as d_file: + writer = csv.writer(d_file) + writer.writerow( + [ + "time(s)", + "Pressure(pascals)", + "z_velocity(mm/s)", + "plunger_velocity(mm/s)", + "threshold(pascals)", + ] + ) + writer.writerow( + [ + "0", + "0", + f"{run_args.z_speed}", + f"{plunger_speed}", + f"{lqid_cfg['sensor_threshold_pascals']}", + ] + ) + for i in range(len(data)): + writer.writerow([f"{i*0.004}", f"{data[i]}"]) if not run_args.ctx.is_simulating(): for probe in data_files: if _test_for_blockage(data_files[probe], lps.sensor_threshold_pascals): diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 0905e1cdefd..981c070db6d 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -1147,6 +1147,8 @@ def _ul_per_mm_of_shaft_diameter(diameter: float) -> float: pip_nominal_ul_per_mm = _ul_per_mm_of_shaft_diameter(1) elif "p1000" in pip.model.lower(): pip_nominal_ul_per_mm = _ul_per_mm_of_shaft_diameter(4.5) + elif "p200" in pip.model.lower(): + pip_nominal_ul_per_mm = _ul_per_mm_of_shaft_diameter(2) else: raise RuntimeError(f"unexpected pipette model: {pip.model}") # 10000 is an arbitrarily large volume that none of our pipettes can reach diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_cavity_ot3_p50_single.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_cavity_ot3_p50_single.py new file mode 100644 index 00000000000..8d56641eb1d --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_cavity_ot3_p50_single.py @@ -0,0 +1,31 @@ +"""Gravimetric OT3.""" +from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType + +metadata = {"protocolName": "gravimetric-cavity-ot3-p50-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOTS_TIPRACK = {20: [2, 3, 5, 6, 7, 8, 9, 10], 50: [2, 3, 5, 6, 7, 8, 9, 10]} +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_1channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.drop_tip(home_after=False) + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py index 6fe882f5370..6787297b382 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py @@ -8,6 +8,7 @@ SLOT_SCALE = 4 SLOTS_TIPRACK = { # TODO: add slot 12 when tipracks are disposable + 20: [2, 3, 5, 6, 7, 8, 9, 10, 11], 50: [2, 3, 5, 6, 7, 8, 9, 10, 11], 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96_20ul_tip-increment .py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96_20ul_tip-increment .py new file mode 100644 index 00000000000..e7e2cd28337 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96_20ul_tip-increment .py @@ -0,0 +1,28 @@ +"""Photometric OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "gravimetric-ot3-p200-96-20ul-tip"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOTS_TIPRACK = { + # TODO: add slot 12 when tipracks are disposable + 20: [2, 3, 5, 6, 7, 8, 9, 10, 11], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_96channel_200", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96_50ul_tip-increment.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96_50ul_tip-increment.py new file mode 100644 index 00000000000..aaebef0f750 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96_50ul_tip-increment.py @@ -0,0 +1,28 @@ +"""Photometric OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "gravimetric-ot3-p200-96-50ul-tip"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOTS_TIPRACK = { + # TODO: add slot 12 when tipracks are disposable + 50: [2, 3, 5, 6, 7, 8, 9, 10, 11], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_96channel_200", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p200_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p200_96.py index 726f5b72533..2847399f1c2 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p200_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p200_96.py @@ -3,11 +3,12 @@ from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "gravimetric-ot3-p200-96"} -requirements = {"robotType": "Flex", "apiLevel": "2.21"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} SLOT_SCALE = 4 SLOTS_TIPRACK = { # TODO: add slot 12 when tipracks are disposable + 20: [2, 3, 5, 6, 7, 8, 9, 10, 11], 50: [2, 3, 5, 6, 7, 8, 9, 10, 11], 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], } diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p50_single.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p50_single.py index 864aa5342a6..25287d0a9e0 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p50_single.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p50_single.py @@ -7,6 +7,7 @@ SLOT_SCALE = 4 SLOTS_TIPRACK = { 50: [3], + 20: [6], } LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/photometric_ot3_p200_96_sz.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/photometric_ot3_p200_96_sz.py new file mode 100644 index 00000000000..479c132ce90 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/photometric_ot3_p200_96_sz.py @@ -0,0 +1,37 @@ +"""Photometric OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "photometric-ot3-p200-96"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOTS_TIPRACK = { + 50: [5, 6, 8, 9, 11], + 200: [5, 6, 8, 9, 11], # NOTE: ignoring this tip-rack during run() method + 1000: [5, 6, 8, 9, 11], +} +SLOT_PLATE = 3 +SLOT_RESERVOIR = 2 + +RESERVOIR_LABWARE = "nest_1_reservoir_195ml" +PHOTOPLATE_LABWARE = "corning_96_wellplate_360ul_flat" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + # FIXME: use official tip-racks once available + ctx.load_labware( + f"opentrons_flex_96_tiprack_{size}uL_adp", slot, namespace="custom_beta" + ) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + if size == 50 # only calibrate 50ul tips for 96ch test + ] + reservoir = ctx.load_labware(RESERVOIR_LABWARE, SLOT_RESERVOIR) + plate = ctx.load_labware(PHOTOPLATE_LABWARE, SLOT_PLATE) + pipette = ctx.load_instrument("flex_96channel_200", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, reservoir["A1"].top()) + pipette.dispense(10, plate["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py index 2cb4dcc1daf..822c6ba326a 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py @@ -6,6 +6,7 @@ requirements = {"robotType": "Flex", "apiLevel": "2.15"} SLOTS_TIPRACK = { + 20: [5, 6, 8, 9, 11], 50: [5, 6, 8, 9, 11], 200: [5, 6, 8, 9, 11], } @@ -23,7 +24,7 @@ def run(ctx: ProtocolContext) -> None: pipette = ctx.load_instrument("flex_96channel_1000", "left") adapters = [ ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) - for slot in SLOTS_TIPRACK[50] + for slot in SLOTS_TIPRACK[20] ] for tip_size in SLOTS_TIPRACK.keys(): tipracks = [ diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p200_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p200_96.py index 092c00e5878..5e999596b28 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p200_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p200_96.py @@ -6,6 +6,7 @@ requirements = {"robotType": "Flex", "apiLevel": "2.15"} SLOTS_TIPRACK = { + 20: [5, 6, 8, 9, 11], 50: [5, 6, 8, 9, 11], 200: [5, 6, 8, 9, 11], } @@ -23,7 +24,7 @@ def run(ctx: ProtocolContext) -> None: pipette = ctx.load_instrument("flex_96channel_200", "left") adapters = [ ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) - for slot in SLOTS_TIPRACK[50] + for slot in SLOTS_TIPRACK[20] ] for tip_size in SLOTS_TIPRACK.keys(): tipracks = [ diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense/lld_test_empty_wells.py b/hardware-testing/hardware_testing/protocols/liquid_sense/lld_test_empty_wells.py index 3adfd8e3208..9dafa3a9803 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense/lld_test_empty_wells.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense/lld_test_empty_wells.py @@ -14,6 +14,7 @@ PipetteChannelType, PipetteModelType, PipetteVersionType, + PipetteOEMType, ) ########################################### @@ -113,6 +114,7 @@ def _setup( major=int(pip_model_list[-1][-3]), # type: ignore[arg-type] minor=int(pip_model_list[-1][-1]), # type: ignore[arg-type] ), + oem=PipetteOEMType.EM if "em" in pipette.model else PipetteOEMType.OT, ) # Writes details about test run to google sheet. tipVolume = "t" + str(TIP_SIZE) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p200_96_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p200_96_vial.py new file mode 100644 index 00000000000..4942f192700 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p200_96_vial.py @@ -0,0 +1,41 @@ +"""lld OT3 P200.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p200-96-vial"} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 + +SLOTS_TIPRACK = { + 20: [3], + 50: [3], + 200: [3], +} + +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_96channel_1000", "left") + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for size, slots in SLOTS_TIPRACK.items(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index c0b49e376bb..69623381d96 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -1,4 +1,5 @@ """Can messenger class.""" + from __future__ import annotations import asyncio from inspect import Traceback @@ -361,7 +362,11 @@ async def _read_task_shield(self) -> None: return except BaseException: log.exception("Exception in read") + await asyncio.sleep(0) continue + else: + log.error("read task finished, this should not happen") + await asyncio.sleep(0) async def _read_task(self) -> None: """Read task.""" diff --git a/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx index 4f442fa42b9..897e74920c8 100644 --- a/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx +++ b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx @@ -1,28 +1,35 @@ // render using targetted component using @testing-library/react // with wrapping providers for i18next and redux -import type * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' import { I18nextProvider } from 'react-i18next' import { render } from '@testing-library/react' -import type { RenderOptions, RenderResult } from '@testing-library/react' import { useHydrateAtoms } from 'jotai/utils' import { Provider } from 'jotai' +import type { + ComponentProps, + ComponentType, + PropsWithChildren, + ReactElement, + ReactNode, +} from 'react' +import type { RenderOptions, RenderResult } from '@testing-library/react' + interface HydrateAtomsProps { initialValues: Array<[any, any]> - children: React.ReactNode + children: ReactNode } interface TestProviderProps { initialValues: Array<[any, any]> - children: React.ReactNode + children: ReactNode } const HydrateAtoms = ({ initialValues, children, -}: HydrateAtomsProps): React.ReactNode => { +}: HydrateAtomsProps): ReactNode => { useHydrateAtoms(initialValues) return children } @@ -30,7 +37,7 @@ const HydrateAtoms = ({ export const TestProvider = ({ initialValues, children, -}: TestProviderProps): React.ReactNode => ( +}: TestProviderProps): ReactNode => ( {children} @@ -38,19 +45,19 @@ export const TestProvider = ({ export interface RenderWithProvidersOptions extends RenderOptions { initialValues?: Array<[any, any]> - i18nInstance: React.ComponentProps['i18n'] + i18nInstance: ComponentProps['i18n'] } export function renderWithProviders( - Component: React.ReactElement, + Component: ReactElement, options?: RenderWithProvidersOptions ): RenderResult { const { i18nInstance = null, initialValues = [] } = options ?? {} const queryClient = new QueryClient() - const ProviderWrapper: React.ComponentType< - React.PropsWithChildren> + const ProviderWrapper: ComponentType< + PropsWithChildren> > = ({ children }) => { const BaseWrapper = ( diff --git a/opentrons-ai-client/src/atoms/ResizeBar/index.tsx b/opentrons-ai-client/src/atoms/ResizeBar/index.tsx index 58ba202eee0..73a8db3f30f 100644 --- a/opentrons-ai-client/src/atoms/ResizeBar/index.tsx +++ b/opentrons-ai-client/src/atoms/ResizeBar/index.tsx @@ -5,10 +5,12 @@ import { POSITION_RELATIVE, } from '@opentrons/components' +import type { MouseEvent } from 'react' + export function ResizeBar({ handleMouseDown, }: { - handleMouseDown: (e: React.MouseEvent) => void + handleMouseDown: (e: MouseEvent) => void }): JSX.Element { return (
) => { +const render = (props: ComponentProps) => { return renderWithProviders() } describe('SendButton', () => { - let props: React.ComponentProps + let props: ComponentProps beforeEach(() => { props = { diff --git a/opentrons-ai-client/src/atoms/TextAreaField/index.tsx b/opentrons-ai-client/src/atoms/TextAreaField/index.tsx index d1c1f0d8c17..1335196971f 100644 --- a/opentrons-ai-client/src/atoms/TextAreaField/index.tsx +++ b/opentrons-ai-client/src/atoms/TextAreaField/index.tsx @@ -1,22 +1,30 @@ +import { forwardRef } from 'react' +import styled, { css } from 'styled-components' import { - TYPOGRAPHY, - useHoverTooltip, - RESPONSIVENESS, - SPACING, - COLORS, - BORDERS, - Flex, ALIGN_CENTER, + BORDERS, + COLORS, DIRECTION_COLUMN, DIRECTION_ROW, - StyledText, + Flex, Icon, - Tooltip, + RESPONSIVENESS, + SPACING, + StyledText, TEXT_ALIGN_RIGHT, + Tooltip, + TYPOGRAPHY, + useHoverTooltip, } from '@opentrons/components' + +import type { + ChangeEventHandler, + FocusEvent, + MouseEvent, + MutableRefObject, + ReactNode, +} from 'react' import type { IconName } from '@opentrons/components' -import * as React from 'react' -import styled, { css } from 'styled-components' export const INPUT_TYPE_NUMBER = 'number' as const export const LEGACY_INPUT_TYPE_TEXT = 'text' as const @@ -27,7 +35,7 @@ export interface TextAreaFieldProps { /** field is disabled if value is true */ disabled?: boolean /** change handler */ - onChange?: React.ChangeEventHandler + onChange?: ChangeEventHandler /** name of field in form */ name?: string /** optional ID of