From fc30a956b554097ca3f2f90695be269360dbfc98 Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 17:08:27 -0400 Subject: [PATCH 01/12] Clearing repo for version 1.0 --- docs/source/_static/.gitignore | 9 - docs/source/_static/favicon.ico | Bin 15086 -> 0 bytes docs/source/_static/upstage-flow.png | Bin 18453 -> 0 bytes docs/source/_static/upstage-logo-medium.png | Bin 1113 -> 0 bytes docs/source/_static/upstage-logo-small.png | Bin 805 -> 0 bytes docs/source/class_refs.txt | 18 - docs/source/conf.py | 86 - docs/source/demo.md | 18 - docs/source/index.md | 93 - .../user_guide/how_tos/active_states.rst | 255 --- .../user_guide/how_tos/communications.rst | 225 --- .../user_guide/how_tos/decision_tasks.rst | 139 -- .../user_guide/how_tos/entity_naming.rst | 128 -- .../source/user_guide/how_tos/environment.rst | 53 - docs/source/user_guide/how_tos/events.rst | 157 -- docs/source/user_guide/how_tos/geography.rst | 254 --- .../user_guide/how_tos/keyvalue_states.rst | 227 --- docs/source/user_guide/how_tos/knowledge.rst | 140 -- .../user_guide/how_tos/mimic_states.rst | 157 -- .../user_guide/how_tos/motion_manager.rst | 208 --- .../user_guide/how_tos/multistore_states.rst | 46 - docs/source/user_guide/how_tos/nucleus.rst | 149 -- .../user_guide/how_tos/random_numbers.rst | 40 - .../user_guide/how_tos/resource_states.rst | 113 -- docs/source/user_guide/how_tos/resources.rst | 83 - docs/source/user_guide/how_tos/routines.rst | 121 -- .../user_guide/how_tos/stage_variables.rst | 114 -- .../user_guide/how_tos/state_sharing.rst | 68 - docs/source/user_guide/how_tos/states.rst | 66 - docs/source/user_guide/how_tos/task.rst | 68 - .../user_guide/how_tos/task_networks.rst | 119 -- docs/source/user_guide/how_tos/times.rst | 42 - docs/source/user_guide/how_tos/typing.rst | 104 -- docs/source/user_guide/index.md | 80 - .../user_guide/tutorials/best_practices.rst | 120 -- .../user_guide/tutorials/complex_cashier.rst | 8 - docs/source/user_guide/tutorials/data.rst | 505 ------ .../tutorials/data_creation_example.rst | 8 - .../user_guide/tutorials/first_sim_full.rst | 8 - .../user_guide/tutorials/first_simulation.rst | 586 ------- .../user_guide/tutorials/interrupts.rst | 369 ---- .../source/user_guide/tutorials/rehearsal.rst | 273 --- .../user_guide/tutorials/rehearsal_sim.rst | 8 - .../user_guide/tutorials/simpy_compare.rst | 472 ----- jupyterlite/.nojekyll | 0 jupyterlite/content/RunCashier.ipynb | 243 --- jupyterlite/content/model/__init__.py | 0 jupyterlite/content/model/cashier_model.py | 296 ---- jupyterlite/content/model/helpers.py | 39 - jupyterlite/requirements.txt | 8 - src/upstage_des/__init__.py | 16 - src/upstage_des/_version.py | 9 - src/upstage_des/actor.py | 1248 -------------- src/upstage_des/api.py | 166 -- src/upstage_des/base.py | 649 ------- src/upstage_des/communications/__init__.py | 5 - src/upstage_des/communications/comms.py | 373 ---- src/upstage_des/communications/processes.py | 44 - src/upstage_des/communications/routing.py | 276 --- src/upstage_des/constants.py | 11 - src/upstage_des/data_types.py | 632 ------- src/upstage_des/data_utils/__init__.py | 12 - src/upstage_des/data_utils/data_recorder.py | 62 - src/upstage_des/data_utils/data_utils.py | 261 --- src/upstage_des/events.py | 897 ---------- src/upstage_des/geography/__init__.py | 29 - src/upstage_des/geography/conversions.py | 254 --- src/upstage_des/geography/geo_types.py | 124 -- src/upstage_des/geography/intersections.py | 275 --- src/upstage_des/geography/spherical.py | 487 ------ src/upstage_des/geography/wgs84.py | 391 ----- src/upstage_des/math_utils.py | 111 -- src/upstage_des/motion/__init__.py | 10 - src/upstage_des/motion/cartesian_model.py | 150 -- src/upstage_des/motion/geodetic_model.py | 207 --- src/upstage_des/motion/great_circle_calcs.py | 144 -- src/upstage_des/motion/motion.py | 493 ------ src/upstage_des/motion/stepped_motion.py | 320 ---- src/upstage_des/nucleus.py | 110 -- src/upstage_des/py.typed | 0 src/upstage_des/resources/__init__.py | 38 - src/upstage_des/resources/container.py | 408 ----- src/upstage_des/resources/monitoring.py | 337 ---- src/upstage_des/resources/reserve.py | 129 -- src/upstage_des/resources/sorted.py | 133 -- src/upstage_des/routines.py | 178 -- src/upstage_des/state_proxies.py | 144 -- src/upstage_des/state_sharing.py | 128 -- src/upstage_des/states.py | 1527 ----------------- src/upstage_des/task.py | 679 -------- src/upstage_des/task_network.py | 327 ---- src/upstage_des/test/__init__.py | 5 - src/upstage_des/test/conftest.py | 64 - src/upstage_des/test/test_actor.py | 327 ---- src/upstage_des/test/test_api.py | 88 - src/upstage_des/test/test_base.py | 162 -- src/upstage_des/test/test_comms.py | 392 ----- src/upstage_des/test/test_container.py | 278 --- src/upstage_des/test/test_data_reporting.py | 308 ---- src/upstage_des/test/test_data_types.py | 143 -- .../test/test_docs_examples/__init__.py | 4 - .../test/test_docs_examples/test_cashier.py | 229 --- .../test_cashier_complex.py | 340 ---- .../test_nucleus_sharing.py | 143 -- .../test_rehearsing_example.py | 151 -- src/upstage_des/test/test_event.py | 595 ------- .../test/test_geography/__init__.py | 4 - .../test/test_geography/conftest.py | 89 - .../test/test_geography/test_conversions.py | 23 - .../test/test_geography/test_intersections.py | 82 - .../test/test_geography/test_spherical.py | 58 - .../test/test_geography/test_wsg84.py | 59 - .../test/test_great_circle_calcs.py | 108 -- src/upstage_des/test/test_integration.py | 313 ---- src/upstage_des/test/test_knowledge.py | 51 - src/upstage_des/test/test_locations.py | 149 -- src/upstage_des/test/test_monitoring.py | 144 -- src/upstage_des/test/test_motion.py | 897 ---------- src/upstage_des/test/test_network_qol.py | 91 - src/upstage_des/test/test_nucleus.py | 76 - .../test/test_nucleus_state_share/__init__.py | 20 - .../test/test_nucleus_state_share/flyer.py | 77 - .../test_nucleus_state_share/mothership.py | 75 - .../test/test_nucleus_state_share/mover.py | 69 - .../test_refuel_example.py | 300 ---- .../test/test_parallel_task_network.py | 88 - src/upstage_des/test/test_request_cancel.py | 124 -- src/upstage_des/test/test_routines.py | 290 ---- .../test/test_sim_wide_tracking.py | 195 --- src/upstage_des/test/test_stage.py | 69 - src/upstage_des/test/test_state.py | 794 --------- .../test/test_state_and_task_sharing.py | 247 --- src/upstage_des/test/test_state_piggyback.py | 240 --- src/upstage_des/test/test_stepped_motion.py | 222 --- src/upstage_des/test/test_stores.py | 282 --- src/upstage_des/test/test_task.py | 383 ----- src/upstage_des/test/test_task_network.py | 914 ---------- src/upstage_des/test/test_units.py | 34 - src/upstage_des/type_help.py | 16 - src/upstage_des/units/__init__.py | 9 - src/upstage_des/units/convert.py | 91 - src/upstage_des/utils.py | 175 -- 142 files changed, 28204 deletions(-) delete mode 100644 docs/source/_static/.gitignore delete mode 100644 docs/source/_static/favicon.ico delete mode 100644 docs/source/_static/upstage-flow.png delete mode 100644 docs/source/_static/upstage-logo-medium.png delete mode 100644 docs/source/_static/upstage-logo-small.png delete mode 100644 docs/source/class_refs.txt delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/demo.md delete mode 100644 docs/source/index.md delete mode 100644 docs/source/user_guide/how_tos/active_states.rst delete mode 100644 docs/source/user_guide/how_tos/communications.rst delete mode 100644 docs/source/user_guide/how_tos/decision_tasks.rst delete mode 100644 docs/source/user_guide/how_tos/entity_naming.rst delete mode 100644 docs/source/user_guide/how_tos/environment.rst delete mode 100644 docs/source/user_guide/how_tos/events.rst delete mode 100644 docs/source/user_guide/how_tos/geography.rst delete mode 100644 docs/source/user_guide/how_tos/keyvalue_states.rst delete mode 100644 docs/source/user_guide/how_tos/knowledge.rst delete mode 100644 docs/source/user_guide/how_tos/mimic_states.rst delete mode 100644 docs/source/user_guide/how_tos/motion_manager.rst delete mode 100644 docs/source/user_guide/how_tos/multistore_states.rst delete mode 100644 docs/source/user_guide/how_tos/nucleus.rst delete mode 100644 docs/source/user_guide/how_tos/random_numbers.rst delete mode 100644 docs/source/user_guide/how_tos/resource_states.rst delete mode 100644 docs/source/user_guide/how_tos/resources.rst delete mode 100644 docs/source/user_guide/how_tos/routines.rst delete mode 100644 docs/source/user_guide/how_tos/stage_variables.rst delete mode 100644 docs/source/user_guide/how_tos/state_sharing.rst delete mode 100644 docs/source/user_guide/how_tos/states.rst delete mode 100644 docs/source/user_guide/how_tos/task.rst delete mode 100644 docs/source/user_guide/how_tos/task_networks.rst delete mode 100644 docs/source/user_guide/how_tos/times.rst delete mode 100644 docs/source/user_guide/how_tos/typing.rst delete mode 100644 docs/source/user_guide/index.md delete mode 100644 docs/source/user_guide/tutorials/best_practices.rst delete mode 100644 docs/source/user_guide/tutorials/complex_cashier.rst delete mode 100644 docs/source/user_guide/tutorials/data.rst delete mode 100644 docs/source/user_guide/tutorials/data_creation_example.rst delete mode 100644 docs/source/user_guide/tutorials/first_sim_full.rst delete mode 100644 docs/source/user_guide/tutorials/first_simulation.rst delete mode 100644 docs/source/user_guide/tutorials/interrupts.rst delete mode 100644 docs/source/user_guide/tutorials/rehearsal.rst delete mode 100644 docs/source/user_guide/tutorials/rehearsal_sim.rst delete mode 100644 docs/source/user_guide/tutorials/simpy_compare.rst delete mode 100644 jupyterlite/.nojekyll delete mode 100644 jupyterlite/content/RunCashier.ipynb delete mode 100644 jupyterlite/content/model/__init__.py delete mode 100644 jupyterlite/content/model/cashier_model.py delete mode 100644 jupyterlite/content/model/helpers.py delete mode 100644 jupyterlite/requirements.txt delete mode 100644 src/upstage_des/__init__.py delete mode 100644 src/upstage_des/_version.py delete mode 100644 src/upstage_des/actor.py delete mode 100644 src/upstage_des/api.py delete mode 100644 src/upstage_des/base.py delete mode 100644 src/upstage_des/communications/__init__.py delete mode 100644 src/upstage_des/communications/comms.py delete mode 100644 src/upstage_des/communications/processes.py delete mode 100644 src/upstage_des/communications/routing.py delete mode 100644 src/upstage_des/constants.py delete mode 100644 src/upstage_des/data_types.py delete mode 100644 src/upstage_des/data_utils/__init__.py delete mode 100644 src/upstage_des/data_utils/data_recorder.py delete mode 100644 src/upstage_des/data_utils/data_utils.py delete mode 100644 src/upstage_des/events.py delete mode 100644 src/upstage_des/geography/__init__.py delete mode 100644 src/upstage_des/geography/conversions.py delete mode 100644 src/upstage_des/geography/geo_types.py delete mode 100644 src/upstage_des/geography/intersections.py delete mode 100644 src/upstage_des/geography/spherical.py delete mode 100644 src/upstage_des/geography/wgs84.py delete mode 100644 src/upstage_des/math_utils.py delete mode 100644 src/upstage_des/motion/__init__.py delete mode 100644 src/upstage_des/motion/cartesian_model.py delete mode 100644 src/upstage_des/motion/geodetic_model.py delete mode 100644 src/upstage_des/motion/great_circle_calcs.py delete mode 100644 src/upstage_des/motion/motion.py delete mode 100644 src/upstage_des/motion/stepped_motion.py delete mode 100644 src/upstage_des/nucleus.py delete mode 100644 src/upstage_des/py.typed delete mode 100644 src/upstage_des/resources/__init__.py delete mode 100644 src/upstage_des/resources/container.py delete mode 100644 src/upstage_des/resources/monitoring.py delete mode 100644 src/upstage_des/resources/reserve.py delete mode 100644 src/upstage_des/resources/sorted.py delete mode 100644 src/upstage_des/routines.py delete mode 100644 src/upstage_des/state_proxies.py delete mode 100644 src/upstage_des/state_sharing.py delete mode 100644 src/upstage_des/states.py delete mode 100644 src/upstage_des/task.py delete mode 100644 src/upstage_des/task_network.py delete mode 100644 src/upstage_des/test/__init__.py delete mode 100644 src/upstage_des/test/conftest.py delete mode 100644 src/upstage_des/test/test_actor.py delete mode 100644 src/upstage_des/test/test_api.py delete mode 100644 src/upstage_des/test/test_base.py delete mode 100644 src/upstage_des/test/test_comms.py delete mode 100644 src/upstage_des/test/test_container.py delete mode 100644 src/upstage_des/test/test_data_reporting.py delete mode 100644 src/upstage_des/test/test_data_types.py delete mode 100644 src/upstage_des/test/test_docs_examples/__init__.py delete mode 100644 src/upstage_des/test/test_docs_examples/test_cashier.py delete mode 100644 src/upstage_des/test/test_docs_examples/test_cashier_complex.py delete mode 100644 src/upstage_des/test/test_docs_examples/test_nucleus_sharing.py delete mode 100644 src/upstage_des/test/test_docs_examples/test_rehearsing_example.py delete mode 100644 src/upstage_des/test/test_event.py delete mode 100644 src/upstage_des/test/test_geography/__init__.py delete mode 100644 src/upstage_des/test/test_geography/conftest.py delete mode 100644 src/upstage_des/test/test_geography/test_conversions.py delete mode 100644 src/upstage_des/test/test_geography/test_intersections.py delete mode 100644 src/upstage_des/test/test_geography/test_spherical.py delete mode 100644 src/upstage_des/test/test_geography/test_wsg84.py delete mode 100644 src/upstage_des/test/test_great_circle_calcs.py delete mode 100644 src/upstage_des/test/test_integration.py delete mode 100644 src/upstage_des/test/test_knowledge.py delete mode 100644 src/upstage_des/test/test_locations.py delete mode 100644 src/upstage_des/test/test_monitoring.py delete mode 100644 src/upstage_des/test/test_motion.py delete mode 100644 src/upstage_des/test/test_network_qol.py delete mode 100644 src/upstage_des/test/test_nucleus.py delete mode 100644 src/upstage_des/test/test_nucleus_state_share/__init__.py delete mode 100644 src/upstage_des/test/test_nucleus_state_share/flyer.py delete mode 100644 src/upstage_des/test/test_nucleus_state_share/mothership.py delete mode 100644 src/upstage_des/test/test_nucleus_state_share/mover.py delete mode 100644 src/upstage_des/test/test_nucleus_state_share/test_refuel_example.py delete mode 100644 src/upstage_des/test/test_parallel_task_network.py delete mode 100644 src/upstage_des/test/test_request_cancel.py delete mode 100644 src/upstage_des/test/test_routines.py delete mode 100644 src/upstage_des/test/test_sim_wide_tracking.py delete mode 100644 src/upstage_des/test/test_stage.py delete mode 100644 src/upstage_des/test/test_state.py delete mode 100644 src/upstage_des/test/test_state_and_task_sharing.py delete mode 100644 src/upstage_des/test/test_state_piggyback.py delete mode 100644 src/upstage_des/test/test_stepped_motion.py delete mode 100644 src/upstage_des/test/test_stores.py delete mode 100644 src/upstage_des/test/test_task.py delete mode 100644 src/upstage_des/test/test_task_network.py delete mode 100644 src/upstage_des/test/test_units.py delete mode 100644 src/upstage_des/type_help.py delete mode 100644 src/upstage_des/units/__init__.py delete mode 100644 src/upstage_des/units/convert.py delete mode 100644 src/upstage_des/utils.py diff --git a/docs/source/_static/.gitignore b/docs/source/_static/.gitignore deleted file mode 100644 index 58960de..0000000 --- a/docs/source/_static/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore -!*.html -!*.css -!*.js -!*.png -!*.ico \ No newline at end of file diff --git a/docs/source/_static/favicon.ico b/docs/source/_static/favicon.ico deleted file mode 100644 index 3269b79c529dafcd6f4b68822c79c60fa89800d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeHOOK%)S5U#wi$(sc7A`cTgQC=83yFI;rBrzc|u@|BUDLf($0VEWZM1VpBNI;Sc zca9vmaN?`f&G=}^2a4sx4vL?{a#}6X5IsC^wBPw z&a({99$c*X#c1qSrk}1PV%M|p`nWBAq0_XZYWO_Eo073x8NR#LEyD-*SWiV`7yNaa ztQ^1QLr(bB@~iCKyS*Yq3UqY;g|8DetV7ehhMP*cBS8mw3(p& z$p4D=RfV2wyxtkR9pC!Lw^=o=I$;<5ni`eiS9~xd{wAw`_F-;(-Wk6~P1w+I6*92m z=?wUrn5Xs%4!%pPVHandWvJ0;+4y;khk(85h`-5G{A>^dbh;7q%Yu^(kTux0jQ?T# z`}1PA{9#Av12*nbG;Ox+%AiO`c4~ zE<9y^2mZ6EGL+~~h%Kk+z6tws_P=<7wgNx&kzz}e^h_2#$JK@9)Z+2uyC~ij*Ea4s z#=>YS{u2BNzRYX5zKnUp`ZYnC54)8KV*z6-;Ty<&RnVVc_1CUQ?3x$xU3OY5c_@$3 z5Z2rCi85$1Ye=5{r46e`6J7XAz;5xtj-F1$k8fn_W~EHLioKeVC625iB zcvf`U*2_XMFP<~_8@wRjB1_vmbs@vm*UG{k|SP&$gsG zRlmbn==QahImWG;b!q)$%);*q%twkYh<%mDpyG}qeeEgg*wX#+OPB`(zIFkB zqA-}89pGFs=xaB5frfH+)wgCHzP6%+eFKt{amd#`m9v|^He>83{rL!ML|?(=WDYw* z>;ZFXMt>qFyV9E(Ut7~Ge2h%~MCm{ZbZC5SS<4*#i2|%DYl^Y32X^#AWej@IG5m{T zkCQ(UR*CVoGwy4`eGKjg8W?C`pn)s2<^KXPTdps`&Lh6PJOf9>f>w)c*t4vIoG-;<$dSW^Q|kEKi25U+x797i z+BWcyj{jmT+Ip@O@vH-pvMX3*TkddQ;4gWD-MI`oa;Rc*6Y3Iw3-h)2W*_X)R3wHB zIZ&|sb=|oIj}m$N7?9@!o15>#kRgYuD>p!5?Fn*Q-j;&@&My8La*)`Atk&*j-p|0> z$AEF)g~1`_*NV-X7!B5ue7pq%Y(eIQQymx_a)2an!N^`mWHE=_R(^wFoHk%yxX^(i zWp4`p6Q&g8ZE?UB_J;V!=HQ6Dm{E@vmD}>R6goh>zdbJ`%*SRfFG+68Td0XSXhME& z8$(GsFhL$^|6`5Vf^V^CF!U+r1m4dX{*r^zrDc8x=7r(MG4ACe+n`Q14aljAe?7dzyo@_J zdg;+6=M-`*F@oZsQ&jq^1KGDc9EFcH$KIc3S$}$+_05miyLN;1j(Ox&p8%A^3w{dn z$L;k9o23+-zXXLjWouGh{v cIUIhT<=*H5dx|V=s;JvK`^4CVvF9oO1Jqh>;s5{u diff --git a/docs/source/_static/upstage-flow.png b/docs/source/_static/upstage-flow.png deleted file mode 100644 index 68e48427670c5e5e58a11da4105ce23afb4500cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18453 zcma%@WmFtZ^yW!|OVA*}-Q5Z97F-6mV8JaoB)Gc-4*`P1;0_tw9R_!I32xiG|J^Tp z&YrU$kfFP)tGl|Z?tPx$C0s>G1`YWmG7Jn1nw+ep8u0T72Ih@0B0TV0jM`%m@Z*h( znv6J1#VFA}@B!9R?6VjQOjRt(qX``F8Oc#r#{~xFefR6vn`FlKgfK8NC32Et8Xktn zSxA|mw_o}?Y8-x=^tLA+Rcn6B^~0i&lM0@sVUA$>fR*qi{I`nEWHv2)7aab`vgZVQtVC;w?4TcX4{J%S8 z0&By@he!zugoxqbL*Pr4u|7~^5!q5_(q_8n?&71~N&UcJ`mG?#M~_XbltldLIWF#! zNTu5o0i7ht$nQ=nAfQY#5E4p{5rBq{r_=5eKAgd;zBBB3P8xuQ*e?i=VtXp_ODZy= z_2t>;ZuLb=PH|X-nJXYB3?n>hSa2o8^`hp|T*Mkauz?;UmViR9_13`iYR27svzIif zZiJ7L?D_)()x~)q>5$8q5G^5(Z4#I*-uK~p==NNF`dnmiOr%ot!7#~bvv=^%=Zt^+ zi0s_l28N#Ie-v|s4U{tZihC5=`(jD8#r>5daL8ACE-r>RgD~fNuaEf2 z3zf1GN-Mq=F`1WPZ421R&8PnSNr6TeE9k-THPJ3v8S96t4Bc zKP-ONz3)Vya9Ka6>r<@ywHWG6CDLMlxjwREIbxxM9)lD6AO_iBlkQFp-du z5`KB4Y})^`a_+C<%n>$SJqk9$ch{R>h-6Mbn?e z>KM=Xcno+X_eh-prk)> z@qA!mDp~O^h$0mc=VelLAQtihH!E4TkwQMA-u(bthx0wIv!1NCLpUBK;ZNAVsJETh zN@4QU)-g&I{HNl-Ke>%ttX>#Yo%BY&^(mQ;^Wj@UqLhGpyJ}>;Y`hBf227fecS`-w z&EC(dccBbbA@UQygY#AM7|s8FN2pcEOi3wWI>5BcLJMi_W+yitoQX)`n4zGeR0#XI zw>R8Ow^0MRm`s*rA4VMxI&%A@%Dpl)VeRrKwJ$xTNxjXTQ*9D51wJ<79>Y#fsU z1@$NFdv7)S(MU#--|#JTzWwcl$C&@eVwT=cbzwF&dUH9F&XCgBTf}sPB}?$=5<}?j zB?E*8ue|92T=Db&nYb?BYLc?3%}pDjHT5CRF49$ngv#^>zJnk7W5lBy4&T*d0-J#l z6a+g&s~{3@F_JDfl)}8-g)UNijgN{Kf?3w!G(nt1C-TxNX6Us~Ehj4**Y5iuzde|2 z-W`VN97JOkxGu;ZF!FA4YLXhHGylo{v^?mchDonpk)`9&nR)GCrc|N9etGM#vZFqR z2j96Dktf#psOt07pU>ON?yJefpT70KLl-0ydA`llcVBj!1e-_zGl+H%sxQ}8keRfA zroD>BAk=Zq6og4Z+76bfcuMU7<1*_~v9_KCWQ&p#5YRU)IY$a!^i!%;nMxfZ213XK z%$o*;J|!gU5ja{js;W4GyCwVfbhv4;lw>cgLp~YY%9vOv0XtJuQ)>c~jD3Mu%=f=A z{EdgTOPuW&o@k$!n%zoqPh?1a^9!HW1%SS7I*)KeK{EJ{1Tu&zJ(+167TCDBR0H$` zlf`B-^R1pWt-qK04+UyPIG2xoCe-&PC-c*sRTpdR7!wPX;`-3z@DoGU_wGcfLwhE48p= zd@^;E%W8Jp5_!3cRrE5cPTtY;xt_PYc)T1NQWkm2M=oV+S`Q)Invfw){yI~l1N0TH zk6XaRvqZC$B^14HXrb=VnSNFg{SzCR?7Z;m6N7;39$BeYd4k*FtXwLK!B!x;a2#Vr zqxb?tB@8>^BZ3T4N6zbF2yw3-l`I|>LhgWxgsJ-3r}%MPKH4Qgn+zh&boIK2K@8H{BKxRu1Hb33ky8f1AK z6c=v6nsA}U^jN|TIkuHnG|RU@q|5O7h~1ZR+ptpTi?u7WPoM=~ole_s4))^Vch?gG zq{^a>iZzN8FiR1vW=fKo44O3}Q|-w^#3o}$^53=F1iGB@LoQU2;9{h;aJ;s!1{A&=tC z?)Bb;1g}cl<>;qbRd}OrOIQUcnklnkYh#%~i&nrA0y<&qTeIRc(D$A!9=mjsIPs^H zANZV>qqQ*{w^+jDpb$>=vEN&LpE;k(Jjd)BmjBQ~JD#o=)yhEZcAnu8=SMYT+1#nn zxQ^Q(V?thgCTnZ!g=z|2iD#$dI_Mzps!yI?%Dz$tn@NDZvgqG3xi9@j7Y1N1kG1!l z4522W@T)MfmOT+4ye8kcI1_Y5eczulS8O2n@7IRJ5b>+=a;X~q?wDEg=CT?W)zfPw z)lJ$R$tZkqMnL;WC3oFS6Cy~t3aiku7RlOPgiy*EG`hA(HVK?n{U&nrL)-KHVROt- z89Ao{-Dy7_WJB%hbk#@(HD9%BjR=F@KxEbTCX35qMPH`-m;cQGZBA_rn@6G#Ceu60 zJ@H>LUmR&9TZ}25UY>764EnN!eVcJlj^=C1DN$F*#^4^#y3h~Bo*u!$pzfJ9tDn@h z9_OZa7elOdSH4G8efhxjs5=ZlmRQK1Wrbw%$nc{(+wb0*k=uWL)9Bm0xz`cL1TtDv zU_H{#E|%kW2IbS0HaG1oN{P@W$P)qr!lCBfWuo8*rgx+JL4BR~$kO2KA0f1o@`~oa z1vtuCxw$o!a)irEMU+CvL?P-w3vMps1zp1Xq6qRRm#3JG={(3(c!Cu2C~OePwTuzJ z_}Qzfsy34i*sliLwu@#j^#Bv|m&ts&$wFn)qGz}!u9Y;2M?baP~=L9W1qv>!?oDO>6kiOcMY z!dR6+u-qFby;Ad&+r=T4M5qK#((HrTASQdK^qwLzIR|_&4BWGxLtBAfcG`T;AVOwW z9mp~j8j4OjTXE|D@?hkvT>I?8vN3$HOy0@wvLg%3IId%GaJ@aOoTpg3}BrH7+0 zF=9WaQ4ZH#5>_IATWWLy#uM9mx+p%`l&*-3sBAe`BGh%d_x9FV+KeYClG^8V#3}YMiI)B)N+~K{j~bK3hcCR<49yGE*loBQUNkAW7;KC ziCND)qL6Y%Eu{f}6z&_}?*DDJgo2B!?J-@X+I;qR#A2w6WSrl-HWM}aigE=O{+p2{3hVfIAme*YUi=rCV<)kBPF@9Gb8{VonOI}gwIjbeN)v) zG7b?Rlg&4wS7v3AGRN=fN?WKFvkJzV{|CrPlRwpqbz?*DW8&P;5`LdsM|z#=tu*ns zKb(Y^Xo3;ny5lW3RrK)>KW%5HwJIxGj*dCBQh>BH@+2>k*F)h?17+ot8K{?A+@Y$B zdm*{U0n)@wB#dV!<_@jr(!^_-H`5LT?osAFlT9dLH}%QzG}Dx_7x~IL;%`)EDh%|L z&$~p&eT`icYgsw|`GA|;HCazVQJTq?O!(ejj;jvrTL;Epq&}mEn8)W6s z@$`*3?CIg=u$O8>mfTd-7^KHU-nlJgDwz}(+3WyBY3945S-Sm1kp%_8_;1vVdXXm= z6rNIXEe`UhOT#D6LynEv?$Ay)YhOahkbL_i zbrzKCYm>vZ+rSVD+0o4sVde3|mdAy8PUV^tWb$TEprDZr{srPUF4Q(3V2T01%nRx` ziW!QGe&}})Jit;X&EJ8*23y!0 zAolt7a!*Kt&2(2$j5ecG=R2cU_l z`7df!kTtubZMi3>WWQL%6r`TNqF)0ci)`jG6R!V8fCe#{uxG3tITWNnUPLIcwoQZP zB7+-0Bc_bn1(LIp1)&5!KcNBbLneAVKeqPZ8TsS}mSf(8|6St}V1J*Pye?-3iz7De z_i)U67!?pM1OFoOZ(PDlmspi1Glbyr6cu!J1_V^lWr4p$pq)yN;Mi4+K!^rkKpSRu z?XL|?6b08e6g)uB15CohlHh4+;yL-VkoKd1<1g**e&t^I)yfQCQ8j; zaFGtAnL}Zi#6~~vJj^&PZ48Gl5V%EqInUa#2k+x4@w-M#&fth+5pfYktl2!U^;a_- z+V7Yc6`qp8tk1><L0F^oHv4eM+GlWeV@G{5^)=Qe^a;`|UIC|2>)tMCdIIza zo^dfx27MX9Mo-&6LOk9EqUOIH$;nbhUio*BLPIjr&3rnsV~riM;wwxC?znmOwl4Hov>}Xb%n;%4&tLKyw^PXfpTNR zJf>-w2U{y4m6^Gjp67SADZ>kl-03)pjc+H*)yEN|F?4ti9jv|8%iLSsex7Vu|74Lm zbTPCAX{9V%3h5z79L1b(^$*WT}{jy%G^_^NrikOr)M4FO`XFZ5C>+osZ_&g_cH)!c|T{wnrijORnXV8l7>~ zK|{e2DeQdutIu+`dbN8m9U^#1!NU?_vMV7r7^40I2|b=Px*gu~l+#mpKzb!RmMwJU zt+m|s;To?r^+UJ(w3x8JhX=T1sIR!z_{k4gZ}FG`-OlgHT^`Ix%{47Ah)zK%Rk>o6 zEAA<$?7r{w(^##;&p@+4mcP*I4r$)LN>ASA#zt*V?G5|)Ko`V0^V8irwD{})QrPy}->5)LFCG1{Ag_O_Uy6)1 zg=@=8N;CdsMYHEA4&6VT^~);yt|KeaSi9_1x6=sQA>I~#h@JEMyZeP!laGC;O}iup zg~DE3pONyayym9%(kMNMlcFxVSF8MFl}}H!dzf z;`4j5(Fxo0X1S?Z6!Ji?#iaE#qCqAS=_|5j9Xk)Hz7Q6E4g+En-Bpx6GUov1gw#*h zLT~zta!!YLf31PnFB5sKul*h-5Rs9EFMZAp96#TP#~57m+cps!Z6yY5Lrxd!O0(Zg zSDDE?1-=I9C}Gerb0D4(aP*w7Ha8#s$$qb3Cc?>Q-?$Dh6dCq}80RxLX44(~6Ih=M zdOqjEAfn+_hy}t6w3!d3c%i9{ZsMN=BE474wh+lR;D609ILvxctpOFrFn_ z<_cd{AO(`?J7wvZenFf=;;X#TFPlF+jwL|x_2qKxplT( z0?Q1an%a494v;tIn=V+`f>yD`6%~f!4BNc+U4Uz5sj(cJfWy4Ge^pC?*_QRR^|AuX zud({se5R&K!k>k(TRC_9x0E`8(7kmQ?QVtcRe?siuGFe_JO$>(qJ!SQI#%4AIgb)WXJP;={Ikrj=>TyAT*rut8ObQnYsbdopcG{SofTs(y~IXuf1*hrx`Ohm!$A=>&sGZb7l(cMAvZt- zPl-loSlnGL`%>}U-SyF<8cUf~r}>{AzsFgaVg`@$lhU$VHVWP-^hTx0nVWiJ>fSFg zajMSoBUhvT@}=<8eO@f?{$mVrq&D0*wQjfXy=y$pXWauAzq{=?tfR;h`}<&AcUMym?;F~L*QGabmqcS=RmtFv^0-7%4uvYsyFB|t1wFSxQ1*!c82*XbPw91Dx; z27je^FtV-(Y?^eDASxOmj|Tb*!?J*)Kpz=LrJ9YapL>sW~WVQ-_J1_K%zQb zrlTI(L%`?w>!hWiv{xAS$Myb{j0otuO{3D#0Po`x?MC0{!rE;F*SBHinx#V;xs5Wu ztEWZc2Po$B5nmOC+Z9-fD_X-fy>d#vGC)P9e!oF?7VmUwy4xX<{}M}&GH0N7;vZP~ z--1a>-N`S&Oh%@ncLGGBmDN7M)4pZn(52da+ABgTdiLX4XrH?5=N<*V-<1V}rUu`W znDtb{Z&|60RV$tY7cvBVZsYEkG29dGN6`Dfzqz4z7no9ePsk%Z^LceiQ!@k*IH^chr-8AR7%38Zc)oc&bU+pwGg3q1ZngdNG7#-IWFEZ z{F`5^3E2K2t)GT29l~NxKj1lH#B{Xk|5C2!?ZoS(V>c9Rymu(c<_JhNRud+2m?RJQ zdt!mucX70)zboWO^fO#Ae-Y=7DQ)^;ShsvTuEoa`a^oJ|>@%yW9CB-9WnzOvE`1Ps z`Zu+3aGRju!vbczS$zRt2zxQ2oHzLGO--s@JzbtP*e%NI%u`o?JATm+BcKd~N1NI` zJLd~IHnO!84gzMmYZ`5d>>dY253)1DwYD`T}}DJhwBf2^4$ zTG;fDQ%XQjLnDac=flAmIz4^i%+n)1_t$A*7xZt!fT9Oj2>GynbqhuYzIc6_hG~8J zsKSgO@L?=s!ae0|G;0hz(d=*P+u8nj%DN1%sn5Jwn1qcl+o9E~mI!aVsw?ywlp>P^ z=*05}vgPjTVV1=jzr9({cAtf24k4Dz*H|8Qz8Pw_o}vMyPz0N??7#y*f0e7*o9qX>asIDQC{vE?>Lr!cfjBuWFRLmud%B7Cv6Q970sO6 z0YxBbR1P;QWAHgBLHOh%WX!Z50EaStdLC2)Rf-QKCCML^(@x3H#wA>RGh1rf(|O>1 ztMa4-Eopvv&%EOjpO8>CsyxGD*%fkCSFqT8rJZG&NP|`Ys@j;WwdQmiDIRnqF3{iA zOOhH9O!Y1?dOZ6!jziR97BP-It#bBwj`j8++GK-+_BQ6@uVDF^h=aMg zs>JD25jnufO8|~m_Z2?eiX^kbdHUF74CuKfhUeumUv}v|_C)UYD3?nj5kahQCKoU& z&M7Pgx()rWG5`uv)kIi#)pr{`;DNT4iZnMXRm}^j|8vdWsIoQS-EP$#JkuLof0IaS zAggm(pb+ZCW?byFTGSF3B1-?IzYthen+d~P&qyvN2kjr&#Wn_|WWTtAoBbKtQI?cI zpvLK92?ftE;TVMT5W*9clQYAsahXyCuas)p0!^`R#m|}X@lr`{0)Grvb?aEv(RU=> zl)zqb1FW)xN%X7*@|tlM+X^KBl^1lS!LsC& zHJ0w%7KRKh?k~l#?%@n<()*8d7*};r!}5c&nWPWP1*7Om($VQfW7xk0L)bKv_kW!f zXD;}MH{Z}{igf4-slPe6vo1mYaSbf+8C%suEUAD;^))s1&;+`%Az#FxVwd$Y53X!% zjy)K3PVmPo2>$?tb?R3_;~;7GdCVtYKyp>BXuaUjgfY+FSBksO_Sf!E-Cl^t(&5{+ z*wROT_TNdfbj6FLMR=im!M75YoyfPidtdX_xkY36V&2$mXrJty3I2;q2&W#zVds-66N#(Nq57 z?m0n!s`qX?f3J{o1*EQKXQ1k6=%jtV&yA0SFXm>;#y^Kh!chMvCt!?f4F1I%B?Lw7 zE9W5G`FUK6gWw|~!B8;U(v)2)Ut#nk->6?~5ysDBCCib-k#FJ4Q**L4QdYZ%*&*MX zl!XyI9nmIzG3_HmIS!A{qq3GuwxIKE)V52n% zo}#QAvUZ3MVX%74U9o1n6_5raw`k58pSeXDfB2Rf{XZKqn3nnA11rs7OMlt)k-`#U z;Ge~E8w~Fd626n)5`=G0vnPg2oNRh|H5TaM(6uT{HCUxL21Dd}J(-%f4q-#t=7N7< z5Nw!og?1q!p(o8a94D4K2)j4LV3q~Q!6_PBihGG|KbXsGjTtHJuRAM+IyEiUJ4_{V z>(*Npw`?6Dp(w#q8%~wx<1i9i6g*;6f_mC+Ru}=de_Z_*b=3G`8cE<_8!#N@;)_6x zb!&mUM>lL#@dCE912FKUG?bF!fq7Kiq3rE&A1Fa^<=v>WyaC+*UF^;>hviJvH)oQj zvI+k8z{c}{H1Z_3(Q=4;Rd9N$ON8$m;E(aBKYl8}TYl&Lf`EKR3)~t>f8!Virx>)+ zzqn;pU_Zq3U?e)FB*{R|U-to^aNG@L;Fe-Ay_xrWd2(m$Svwrx`EI9icQB7nZRw`RA28 zYCC#B!7*f&Euc;m?0O6?NrN=xDj+x%0g=5{#EUVk4FoG$Qkg5@%D1S))Fj&Y3IAgj zh&S}t2K;J>TR_@7p!r>W=K<`8m%(}I4Iezy8UY2LvD;}F@LIp9&XixD(f#53Xuj1kX*_}z) zsO=V?HG1795gjY)l;tdk?c*t=%5YY<0HJ+W6~}t6$gr(B`D>#J;^6K8XzbdhKOKUY zqIr7E?bC*&NtJD9L@t9egm>;h7>Tz?8a8UH2i{%q$(k+KvuW^ne!SC)ST|j35YE|M z_u;)Tjf{zzMN`5@R^G{3mVVyl0Nshj1Z+za+y#<(W=-`XNHoqLyR2ru4IcAXYiE<4a5BjyX(=YjdzZ>DAOo)v~RbfX+UG*-@)qH(HP=SWjI|u-%AP9xwJ$O zs(MVR{zY}Zr4p=;>+b!Kigj=oMz1Ct&N_vL_C7MC#E#WnJQ&T4rLIUZ!#T8yMa^+pSBa00^LsK<-pP&Csj7#Hfx>0x zDXNx>UbO}=Qx|2@H5n_jomR^`v(&-2=e0+|_j`G|MRtqzGtml&*yQM>!uqigsZ3M> zvU=7B{M`YJ4uI1+@D004R2FthvI2ZrMj*dQq|Nr|lxNq+IG9I@q@#M^^`ptkfF_Y$ zJtkH9mix49<~#ESQkSUwWXj?fFX@$%SNT%w+U}j-Z5gBxgmr!znG>(1+{)k(-TL`= zlFAV80aM2NCMp*Be5mFJYO<1aTVY(9=sXE($>UgtZ^=Z+tTYqk){`|75}v1ht6Pwm z=owBzimX$ALw(f=EFvr#Fy>l^IabOoeTcjgJX**PPOM)4cD16VjD zF^`lD}@lGx03@=PO!?xB7C^$a(%Y9R`LRxv1JKWDQN zh{a#*9CUOi-?mIge@?vWQ>Wr)mQd=cOxYNap%x+o$>Y6GFnJ_XwbXO}`NE9z2lTdIvuBvu%6Kj5nX`ZQ$ zU7(+u%W?k9gXRC2KW#NF8*@5OJ20pz!+Hs5i$o&OSo$mXLYYrQRwev|Y(`_o^0XI!PjlS46qW zrLvhDC=XdLBB3B9)#gtf5DaI6Ns3S7P59}dMu=bNs00!JriHmap*+CWo@(eNr}qn(zHKmD z0^KY|W`a@hzODsw{O(Zwg3oBDl3oo&*ZzeN#h3caTR3r`bBYA zez!WNT9$g1N%3-8!QUC!BDBgWO%9>`M%M9Ic_(>AB@&hTZK^D#iz<=48cqAb(?TKe z2>T(`vQPI$+wr@ZmoXT>9?m%A1xR$^(M3ofYLXp|xGlx*o9 z6MiU*`p7L0Fet{`8-m&1C;R}B7l%s3H!jcqNLXB$m_N@Pt5C=6lCM)J%;9#=<@wnD ze6e#;zJW|fg&woVN*dj0a6&9#NE1syXXh=*9INCh9g&}@HKtrbMSp0OI8T=J+q}PX zBsW-IZZo2%V6~igAfG(DzP-Jwq3neFpM-MwAUp3usHIj6CKlGW?}oywI|cC(yBw-zNi**J%A9@%i~q^o-- z3^T_6MQ%MgUxyC6Q2~qRCE}{!TG@n2V5aD%yIq`cv2pUG8ZXgE3ZF#Qjv z>Hg2BeBCXlyGzjkSU9}+p^wn`xF{}ZG1)rZt9*}KW7Uv*)Z$|6$yyhYre?3A$7bJD z4f}5yt@oXI3E#vu)01=W%nsq_-KpdTp(U&{@^}f7n#LMMszwaGQgX6Zql~|Ixp#>k zm6wxysTT(+#8(z8$WN_RI)9r+?{`++#E(`P>wW!Q%ZWQ(%vKP&Eodd1x5lxB8qD3{ z`5jBb3e;GwF?cySCU!s&%1y>5+d1Qcg|U`^v)%k>KZWw3gBZRI;j5m;b7aZ^o_OZg6Jg8PgNn*`#4NHvDgLY5)*N|DEq*l?#P+4!lCE(lJEz z=z?e8E8y{~Lp>+IaUZPy#m*yl>5U?2UMoZ=J--jI@;ynD0ynJ#c#11Mbk1v+)b(^_ zS+mwk6$)Zxup_=P&cqS7LnpQ{_Co*oNr;r|NFHvaaakuQrg2D-_}!WqjId=A zQqDyNrOBe;GL18@0T@EH8X8{bbg}v?($~B=R*R5HDh+P>J}--kI$vj#>Uz8|BhQ_Q z&I-5{B;+#hu(f6#{qP|>SO@x12fA%ub+5e#c8CneradT5YxBODJpr#jvIktj2>K`s zT_wd>+Ddxc12-Tqw8T`0Ak@B*Ey`G^+bmXy1_;omezyVKTA4=Rr{Pq9&1fZYw`V#8 zu*5^Lla3cZ171qUtPc3*Q6vj{6IB9ZiMCZoCwzz({y{CQgBgBaN5xA*GC+_R-qdi^ zM3w+3HgMjzu9r-@wX(_wv}Nxn_#QDsa5*gR0RwS@Gs`g;t*HxfRsrlveWf!?Y+C)j zqx_SCz9r2wXG?fEmXOy!@HM`3AZ8gJdHp${QD248y`492U?6#iVCZ|Ld6-p0Mn=lK zr9++5Hh`rKWxn8%lCSUUdOQ`H6#Ujsm|cZw$9?3H(G(s(iuL6^_3$j_%@oLQrf3Lc>p zzFm7q92^hk2XvQl>sQh-X6FZ7HowasB^P85IK@ax(c6d9j>;)3m4nreju=VtwwZl( zdS6V+IYC%eia7uc)Ciq7sh~y|auG>WG?pp+=N85${-)b^QB~?zP;xMz&6Fn*@(CDE zc35YdIdE=_sjV*&NutEY16OB_&2D(G>hh@*JY0je?;u#MR_yJC?Bd!l0E>#P$W z9w=#OB$>_T_YhmOQM+R>J7wb2+MbaBJ0MEnA+dK&x0V@~e>+)9vs0*M6%+N);vUvr zgDL&QHaVo2eEGUx_ba#rcn=F$m}@bfwJZ&{+OUjWUtfc6tq6wzvT(Ky68-2gF}(Lq znHU^7Km0&}y8v=Z3ED=V;Mjj^e>j;MN@c@3m^;sNW?&afP*0$u;}e~I0V{obtXdWr zYM5(#V7+s`l6TAbk&xh}n~a%jO?t;+OCz#7E24yCi8=2tmynwVI?jLQF(U>7IS}>uSoD4(@}aO ziRag|%XVD_aUQimNYfH{Y4>jgzaj`|t}<#6^Qt%JiRm@P>ke&+LRQ zy$)xO2L)=3FQ|m}mm>C@;bXQNJui0#phhpsk+|pCf@%4A@_<)!c`OL$0*PF80msrOJEz@DSS>jX)K_{g%>lLJSUvkP2 z?)#&XYh~_flq~W*M|0ZVB0GlD`N6Q`nGgW(+oMRHf&{OojonV@l8t^5Y@Sl$Lj-Wj zHV2b!3Jh9ok|OoBN;DY@nc1sy9Sd_jOaofaI_Ur&wZb{A5D8P`ID~=e;^A~mQ#`Bi zxxH|7D03lH>U1KnLHLjB{ssU^OKjb&qW|ErUZaro4e`T%vYH~MGiVb9wCUcKggPIZz-{`u$Yn=Q)g0~_QrD=j=>9f`O8qC z=)wApRw-_&2YqqEY<$oB0^9{+P@MakM($ zt9Yiw#}GRt&6a26Xufc7kGf6tuQh* zchXbN?02=p%vo+2i)WOr%xL7ZFYk+Dhn}{=Vu(V{+3iQAp{i z*YiRvH7)?kAF9CbVRBVTEIx2i2s%@_Y;p%kmwk6U_B}s;VbwDBwGKiA>0d9N-r3~z zrDTE7?qn+9hGdBXA#uC_Ut%FA7D#>yXgkN;TKH&YZ1}AT6>x+Wbzb252#E0+3rP+5 zwR-4g08oEUNb!yB|7VXOtZ>(AQW3u;d3J!5=`;%v66?zW_Gf6qWh%Ajd>TZ&-Z1V~ zvGVv8RGcnl71UMl=yvG?Bat0{m>cf@cpm!7QggWMC^nt<5|(Rp-Er95lB&`1Xaoel zqt+IYknlT80<1bUu#zUWoKSTB zSQqE>xUwsty!(d@QDrDdR*A0-#Y8CqY zK$%F~3Tk^7YlnRTpwbK`P%GrSmGWqnKnAv&-zx;Y`j%U>Gp+e==|5yhW~Mg_*&X zfc!NJcmkH&a(8$m2+69Q@;DnGX)IzraMp$6g(|bqWYaS^HKgZ!Fs&7SW~>aX9zez) zf3sU^oRDX~eOn)lMopKLE4s#xGIZ8GlEJG-4{rnmxoG*xCJcuFkX-_5x#Gc5#=fYq zk-LFbXmIahpAyhMa*Z?Q6Zjw;fI-@3CMfSoJ{)BWdAD*H&Nw;&#MFJMwJQT30Ez)- z)f9T~#r6;oLHfA>GirE5L;_;XwAT-aS%7&2@m0YGMlAs1eB3!(?-B@#CGPE8^ATM5 z=rp6LJ@87X0&z=+$pNJ)_d8l-;4DlEkDR$rYmh*#2y&6|*jQ))cX>C^&F2IFc$uGn_Q_X= zP(7dn-@!H0CZ(FC76tgoZ%OcMZ%&q}Sy+?|J=WnEd*VnjRK62v08l^BI^3JeTInU{ zA?6EUa5BK`I>rz-_!PD;;~5z-7Desey2ppp#d)ii?wN(W4>bg>G8KVf^FAP;a5cca z@c`m6t&%s8|NNx>MO8BV?#|njM8LC3v)Hw})&JL%N~a3QRvsTK5pnOY4rwFtSW^Hm z*#Lo%_X~2$gv<&~S=MRKV_9ZKM&Az?OeRf=K=-o<1ysPA__DENeg-n02I7gcbUX9le_pl0m*fBTdG!1f1;bQ2Dr)aS6g;UM0; z-U`zgsEx@7WEV>$0*sC4r-uT7$GC6gFdy9G)6Y+Y0zvsztA%dLh$zZxj|~g^IHDLM zP+J$h4InPnYEO9jrk{wI#O0Kgq1My9cNIrD0ltW%_W8~);TVJxXqfXrx|yf9oxx*Q z@?cQa2wYklE>S6aBh-LERE)boDiZesK;9KhO@E&hZi8qL$t?cJOsXW;jSjUrD(h3) z|kHMRg-h9hcJTK8cCt(bXhGh3FXQKTZq`>*Z(p!A-#4--Gu zrIDuNjrc2)y`39MGWmA9`DoV=h&SBsfMLI?0*X-hHyJLoA9QLgqFWzMS_HEE4D@Or z|GWy~tyi;p&8t}f`L^@b7Ou&7=gp@sukbc`B&BrJ_-52o)~esbXLxjyGUj#Q!8Zp2 z@@lII3ZcvzAe_~8bzku;WH|yHdPeKz=DL&LVqacCJ$;uE?gcyZ3dY%mwu(Xivck*f zhtosok;Km}e(Omp-$$T!<4b1{(kp^wC&wT1_J@rMc)|Mn8OSgAE1GyVP`YYcf`k9>su)pEBpx&X!b}WM9sf?2_4K4^J{u2! zPxzv3lx!OBXI%%1HR=FD~i;Xyqvl~zIsRrgeZ0vdrfxK z9I7^z$Rmz~7{B9klWg z#f}#r3SZBeHJktJe1j6Z+r`Zv8xT0sRx#|kH1=8{qW$gfaqq|q5Iwcyi53A5bu#dN z4FOP6U8yZonfm}IL=A6K#vB$$t0zaSHms{cVqwlrcTt>zt{0ZlHt26vW~iGPX&}Y( zNP@Fqa3Hhe1GoP&T>1y^RlqxC{N#Qztx$EA$|f3AC$-i%Fr9}7fJ)0I(v1WVuT(+1 zjzatEe#+Q>o60ttSEVWWD@;%X1_rsC%F+&yo_=lkS%I(n-C9w%$+-ZRjk4d0PP|dH zOU>1_Ryd}Y^kd|b!BXbmgvpY~D{5B5DeAr_F6&82Xr9R37Pf&b*#$5)Wm&tLVju>; zXP#_4tKa7Jhz068R9?QWwzpMWs?c8Wf)xd)1@{Ek>J=hXFj3L?0hTMKUc*OOS@1mQ zTcnfQYgGqfECz-B*Um=;z1tb1bh`%ddi_G zGZJj<8q!VIUG1mwaW3aASzjhNWHcj>w@aif?0q${!tMuiI5_a9>ob0HE@I<6&3t>e z9Y6+EJ{^FzQM(K(`+$oHF~O9RXavkrEY-D6=T-w&DG=gYo4%^B5MPj>WX8;IHMTk2 z*vBT4JM$0BxXDjEwK(cO&#W9tw}zbR6-MeH^*fnE{97ZITuWp#$h1NoQ|| zZHZ6gPXmH03|bC6ddGxrGEwo_{>n3ZoN0OwRj>G5j#LBCFrZjaMkBL$uIwcdH+H)= zqZ12g@FwACv{Gw@uyOccOnj`uwe=APe^(g(^$$RsRVzB4y%=X4g*mw*GdpBRAcnnN^w1Lga$hcO&Ak-&7ZH;2K zpbmNkTRh_^JRW}P97y{GVhLx{B{2|WXK@dPXO_4`P0Nc1WCdElSgLz}YtA+ z{n>T0P9bX|xB3mEcYpu7hOl##gc49mexWmJZ>?)slgM%dJT`&hC=p-#z(A(5Pm<=h z|E84d^kgM)DmbRDkSPErKk$kHd&$_8X#(G}`ykZVBui0OH~nx{&%uozvjwn3HeYGP za-mU~@R<)dI9nfueHxQDoUe`-p_)z3=BXDPWjKK%>#`_^?_I=^RKu+q=%p1f+Y|i;4O+hGcymG@$!(E; zG=^(#4b4r^+x`rf2tuHd!{FH&V(kFRS097l(t^k~-k5fGVwGT@W1;T@1&-r^Y>1xX zghQ6f-5%!W}Pu=`LAQ{Iz)sd_GGLYa!>Sjb1 zkyk11Z}ZK?3iA*kY~7%^kZ!bRi66LSXpoBH8Z4Dz;)6htsRhCv1qkF0Y1U^UhKV)_ z|JQ}Z!cz*zgJb)+0sWJ^UjxiWp~jf+&i#wA?714@3!&U4C5g$XDE4$Rh!lKs*>>=&Hq=au5xEB^ zQxQ|3v`Qq&B@@R5UQJrgcOOy}O6qW={IGCISW311i}hM{-th=O7v;CW!E|77OtUUk zSjvSnyM0UkyC(Q34&LqJC8d#93^t}G!lNVLM^6}3@9Yc+HlpQD?x`v3Mxml&5^Xnf zmi=g;31XNY*01kc(~mM{P67T;!iPgCg){|;d29 zX$tF2A}Ja6_;@J|aKmXp*|Y4IFJUoSCMaAS3FBOW=2r(VhH=0UPZlU#eTCQWGQQ=F zGJb)3tA4cXElJ{4)S#GAFRG?);C!G_B6vNAXc7ZsDrHNnM3idUvytz(u82Z}mEbAt zzs$_pC*kP$M^i~P4Wl&N^}0n*@1kyuQ7!1MW%fOsX~Rv`Jd_j1_OiHj>PjT+UL<;%a+78BPuF;QgTz=1k+ zOPeO?pl4 zxo4oVw3+7(A3j_Nsl0UQ7qV*AGTFKFPdRYlh?Fi}UY>n+q!#nRgU4men4$9Jmz(rC zd-fzqojUc^HLRO96j+R4{{bSp+OY8?2 z!JY@WztlSs3fA_js~SpdY;iep;+PU?<}rbR#D+@Lbx$lF$0jDaD= zqf3qvg0~&mO9-xkL*nvkY`;@ywaCP};Zesw`=+ zeDcY+^4xPXv~|X{CGX0W%gKrr8>C#hit2r4s0-@9Wy+M0AAa~l3HECBK3Q(Qxwi}$ z@UX7q7B5~>iMp-2p8##4Uw?g(a=ZU2RztzVSXhgkEG$sP#L`k<<=a9~*|o^xlG+0& z5Hap6?kyIWzcfyf2fB3gigg;sPDI=J2359yZH|ADcv1q)+g z&jZAT^}@Q*vYVY)fpm^xvju5eV9vv;A})8a0_p7MJomggj&n)3#R}w!`0e}2Nvwu~ zhY_s7;2?}(4F(5c1Zyxj2qRd7!9f_o8VnA?2-aY55Js>DgM%=Fbt8a{{|XZNLuI}+ zI3FWe3s;6q0)y0(&A)=Qsn5m`Mzg+7!N3enwD@}QVcD|wgt*OoD=;u(s>}UlrFgk8 zE5+bcj9?802Vq=dgTX-!U)!2a1chY27_Y|`9Gjp^~Z`iye(v!Apn97LBC!N2$_++aUIEtbhQXo z2pSGVKM_adgq)2?ssWLhtZz_{NchT#18G%pM8S4%+D;0giP9E?aD z)|hDGoVhpxQXC#8&SWGbJ|UiD9~lWEv^Agi^(Z7{9sZ)j20iX21L*NDEw&o)kPfR! z1nC;^-ALSH15O+9upal2Bbf(rc|2(YxxNFe-Bt-AAulsMO;Y@n*`O;;N~QQutD<<( z1=1}RRq~eSOS8q+I6kG0+P+h(NYcnk1SPwS25nBti@GRESfYPcovRnToRFrCEw}A( z`diX%ZJlg>T;nEX?!MBP?fniP3E~|rDKbMqkYXi=7{kc z7U69{OuRJJQr!I;=}9Sbfx{TO^z*FqyZm_ntx5loI$psA_V(%#oO#azEQw3zT^KDr165QH)PRE=g;mR{UkV=bVKCtzczgDgV~1RF~QQ}D{yoAkv#00rvd?4UgiCX zfxDS9@5E=bilwH(!arAUzIEpJ@Q<5^vx1MDe>1D_$G3_T^cS*Ta%Y=ZOEp_dvu=~C z*-aKutFn#TRJv`d97nd^q|P>JG?^;3Nu~PcR*?JE{u4BMYdv*s|1Y>a-Z4f5FK`K! z-a6JtH@m^-^C>+|hgw`T<5qf`>rXA_B@&X9BRM>5gBz-t7M8Ae%XiVQdtBb8qN=PA zA?#^)RI_&4Cuf?yb)E*doNZ&=au37009sGqS>7Nz?99lxxH~jO&Dnvk5G!3`i9%A+ z_Z(j$H~Y20TE60fLms+z^{2_H$>d34$ZtJUtb_&GGwgDquAf8zWwDt@3Tux10|SJ* A`2YX_ diff --git a/docs/source/_static/upstage-logo-small.png b/docs/source/_static/upstage-logo-small.png deleted file mode 100644 index 4f7be6426729dc7574f668cfb06ac2310f5fc5a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 805 zcmZ`%T}YE*6h3RSZR%_?&1pHln##yrb928+Zno(+on~|Xb(3Fn)8Wt7{Ay_wLp01T zEW)r9%DPdD=#P*HLL3oJ`oV;PZZZP%B4k|@C7j-A&{Y@jdCqyx^PF=o-V3!g6{5tf zL;w)kX{(cIm@KEY%i(2O~)vOaiGXH|?wj=*t5L4*>ikRCpEOqzYiI z1;8{0kP(=8dZdIjrfMB78zMq60w9llG$0a9J%aYHGR_d5 z*hw_w2C);PW|P$6Z!(IMTVgY!ag!(H*0g*qCVDNR7GLPlt4Em@e-f_&zw0oh$1Xj# z>u@L07m*wB^?>0kxmnLTYwAF>jhClMm)SU+B>O&fGPl%`LfJ%0fg@91;wVp~s{}da zDaD55qU_S6XB*_^vW`G!`TiTXrf-uGoa0p$Rv1dirb!_T(9JyrC*zMZVr;G;37@rB z+xRO9LUH;YTjgMUZ?D~IaY>ilEmW#XJsG>!{3h-G!^zkOqajl|JDa+FeZ60C@SNh9 zxt5k%FAfb`c#%Qg+~a{9tF@A*v-Zm6`TVrYjbf*(U!aIC%uEGmgu>}VqNPg-!|jPe zp>OW)($&%Bx66_h=DcuqmozJLeC#^&Lk5kx{EuSU9W6N`m$fyqtqkiixq}`8pjH(c z3RKzxmC;qGGpP$r8cn`RZBnV8f0~bfn}3Ebf2+^i`+vi$XJ?)eLuPzKo!`rbn4kwj zp^(zo-O=M_0v@G5*f##HB#VgB<06-j?esu>poeAJJXASz%IEfXSJWSf5` -.. |EnvironmentContext| replace:: :py:class:`~upstage_des.base.EnvironmentContext` -.. |UpstageBase| replace:: :py:class:`~upstage_des.base.UpstageBase` -.. |NamedEntity| replace:: :py:class:`~upstage_des.base.NamedUpstageEntity` -.. |LinearChangingState| replace:: :py:class:`~upstage_des.states.LinearChangingState` -.. |CartesianLocation| replace:: :py:class:`~upstage_des.data_types.CartesianLocation` -.. |GeodeticLocationChangingState| replace:: :py:class:`~upstage_des.states.GeodeticLocationChangingState` -.. |ResourceState| replace:: :py:class:`~upstage_des.states.ResourceState` -.. |SelfMonitoringStore| replace:: :py:class:`~upstage_des.stores.SelfMonitoringStore` -.. |DecisionTask| replace:: :py:class:`~upstage_des.task.DecisionTask` -.. |Routine| replace:: :py:class:`~upstage_des.routines.Routine` diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 1dc7925..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,86 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html -import os -import sys - -from datetime import datetime - -import upstage_des - -sys.path.insert(0, os.path.abspath("../../src")) - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/mast er/usage/configuration.html#project-information - -project = "UPSTAGE" -copyright = f"{datetime.now().year}, {upstage_des.__authors__}" -author = upstage_des.__authors__ -release = upstage_des.__version__ - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "sphinx.ext.coverage", - "sphinx.ext.githubpages", - "myst_parser", -] -templates_path = ["_templates"] -nbsphinx_allow_errors = True -add_module_names = False - -autodoc_default_options = { - "ignore-module-all": True, -} - -# ---- MYST options ----------- -myst_enable_extensions = ["colon_fence", "substitution"] -myst_heading_anchors = 2 -myst_substitutions = { - "rtd": "[Read the Docs](https://readthedocs.org/)", - "Actor": "{py:class}`Actor `", - "State": "{py:class}`~upstage_des.states.State`", - "Task": "{py:class}`~upstage_des.task.Task`", - "TaskNetwork": "{py:class}`~upstage_des.task_network.TaskNetwork`", - "EnvironmentContext": "{py:class}`~upstage_des.base.EnvironmentContext`", - "UpstageBase": "{py:class}`~upstage_des.base.UpstageBase`", - "NamedEntity": "{py:class}`~upstage_des.base.NamedUpstageEntity`", - "LinearChangingState": "{py:class}`~upstage_des.states.LinearChangingState`", - "CartesianLocation": "{py:class}`~upstage_des.data_types.CartesianLocation`", - "GeodeticLocationChangingState": "{py:class}`~upstage_des.states.GeodeticLocationChangingState`", - "ResourceState": "{py:class}`~upstage_des.states.ResourceState`", - "SelfMonitoringStore": "{py:class}`~upstage_des.stores.SelfMonitoringStore`", - "DecisionTask": "{py:class}`~upstage_des.task.DecisionTask`", - "Routine": "{py:class}`~upstage_des.routines.Routine`", - "PointToPoint": "{py:class}`~upstage_des.communications.comms.PointToPointCommsManager`", - "RoutingTable": "{py:class}`~upstage_des.communications.routing.RoutingTableCommsManager`", -} - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "pydata_sphinx_theme" -html_static_path = ["_static"] -html_logo = "_static/upstage-logo-medium.png" -html_show_sourcelink = False -html_theme_options = { - "show_nav_level": 2, - "navbar_center": ["navbar-nav"], - "logo": { - "text": "UPSTAGE", - }, - "show_toc_level": 1, - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/gtri/upstage/", - "icon": "fa-brands fa-square-github", - "type": "fontawesome", - }, - ] -} diff --git a/docs/source/demo.md b/docs/source/demo.md deleted file mode 100644 index 4be2e21..0000000 --- a/docs/source/demo.md +++ /dev/null @@ -1,18 +0,0 @@ -# Demo - -The demo is loading. - - - - diff --git a/docs/source/index.md b/docs/source/index.md deleted file mode 100644 index 9e3144d..0000000 --- a/docs/source/index.md +++ /dev/null @@ -1,93 +0,0 @@ -# ![logo](_static/upstage-logo-medium.png) UPSTAGE - -The Universal Platform for Simulating Tasks and Actors with Graphs and Events (UPSTAGE) library is a Python framework for creating robust, behavior-driven Discrete Event Simulations (DES). -The primary goal of UPSTAGE is to enable the quick creation of simulations at any desired level of abstraction with built-in data recording, simulation integrity and runtime checks, and -assistance for the usual pitfalls in custom discrete-event simulation: interrupts and cancellations. - -UPSTAGE - which is built on the [SimPy](https://simpy.readthedocs.io/en/latest/) library - contains two primary components that are assembled to create a broad array of simulations. - -The components are {{ Actor }} - which contain {{ State }} - and {{ Task }}, which can be assembled into a {{ TaskNetwork }}. Actors can have multiple networks running on them, their states can be shared, and there are features for interactions between task networks running on the same actor. Those tasks modify the states on their actor, with features for real-time states that update on request without requiring time-stepping or modifying the existing events. - -```{image} _static/upstage-flow.png -:align: center -``` - -Additional features include: - -1. Context-aware {{ EnvironmentContext }}, accessed via {{ UpstageBase }}, enabling thread-safe simulation globals for the Stage and Named Entities (see below). -2. Active States, such as {{ LinearChangingState }} which represent continuous-time attributes of actors at discrete points. -3. Spatial-aware data types like {{ CartesianLocation }}, and states like the waypoint-following {{ GeodeticLocationChangingState }}. -4. Geodetic and cartesian positions, distances, and motion - with ranged sensing. -5. {{ NamedEntity }} in a thread-safe global context, enabling easier "director" logic creation with fewer args in your code -6. The stage: a global context variable for simulation properties and attributes. This enables under-the-hood coordination of motion, geography, and other features. -7. Rehearsal: Write planning and simulation code in one place only, and "rehearse" an actor through a task network using planning factors to discover task feasibility. -8. All States are recordable, and some record dataclass and dictionary values. -9. A {{ Routine }} class for building reusable event behaviors to simplify {{ Task }} coding. -10. {{ PointToPoint }} and {{ RoutingTable }} communications handlers -11. Numerous runtime checks and error handling for typical DES pitfalls: based on years of custom DES-building experience. -12. And more! - -```{note} -This project is under active development. -``` - -## Demo - -Try one of these demos in your browser with JupyterLite. - -```{toctree} -:caption: UPSTAGE Demos -:maxdepth: 1 - -demo.md -``` - -## Installation - -In a suitable Python environment (3.11+): - -```console -(venv) $ pip install upstage-des -``` - -### Installation from source - -Alternatively, you can download UPSTAGE and install it manually. Clone, or download the archive and extract it. From the extraction location (and within a suitable Python environment): - -```console -(venv) $ python -m pip install . -``` - -(or just `pip install .`) - -## User Guide - -```{toctree} -:caption: Guide -:maxdepth: 3 - -user_guide/index -``` - -## Contributing - -To contribute to UPSTAGE, or to learn the steps for building documentation, running tests, and putting -in PRs, see [CONTRIBUTING.MD](https://github.com/gtri/upstage/blob/main/CONTRIBUTING.md)) - -## License and Attribution - -This software is licensed under the BSD 3-Clause. Please see the `LICENSE` file in the repository for details. - -## Reference - -This section contains information-oriented reference materials for developers -looking to understand the UPSTAGE software components and its API. - -The API documentation is auto-generated. - -```{toctree} -:caption: API -:maxdepth: 2 - -auto/modules.rst -``` diff --git a/docs/source/user_guide/how_tos/active_states.rst b/docs/source/user_guide/how_tos/active_states.rst deleted file mode 100644 index 58f8d4b..0000000 --- a/docs/source/user_guide/how_tos/active_states.rst +++ /dev/null @@ -1,255 +0,0 @@ -============= -Active States -============= - -Active States are an UPSTAGE feature where states are told how to update themselves when requested, -while not having to modify or alter the timeout they are changing during. - -For example, a fuel depot may dispense fuel at a given rate for some amount of time. -An employee may monitor that level at certain times. UPSTAGE allows the state to hold its own -update logic, rather than the employee code needing to know when the fuel started changing, at what rate, etc. - -Active states are stopped and started with :py:meth:`~upstage_des.actor.Actor.activate_state` and -:py:meth:`~upstage_des.actor.Actor.deactivate_state`. - -Active states are automatically stopped when a Task is interrupted. - -Linear Changing State -===================== - -The linear changing state is a floating-point state that accepts a rate parameter. - -.. code-block:: python - - class DrinkDispenser(UP.Actor): - vessel: float = UP.LinearChangingState() - - class Dispense(UP.Task): - def task(self, *, actor: DrinkDispenser): - time: float = self.get_actor_knowledge(actor, "drink time", must_exist=True) - rate: float = self.get_actor_knowledge(actor, "flow rate", must_exist=True) - - actor.activate_state( - state="vessel", - rate=-rate, - task=self, # this is for debug logging - ) - # OR, to get argument hints - # actor.activate_linear_state(...) - yield UP.Wait(time) - actor.deactivate_state( - state="vessel", - task=self, - ) - # OR: - # actor.deactivate_all_states(task=self) - -If you set up the code to run like this: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - fountain = DrinkDispenser( - name="Fountain", - vessel=100.0, - ) - - task = Dispense() - fountain.set_knowledge("drink time", 10.0) - # It dispenses 2 units per time unit - fountain.set_knowledge("flow rate", 2.0) - - task.run(actor=fountain) - - env.run(until=5.0) - print(fountain.vessel) - >>> 90.0 - # Run until no more events are queued - env.run() - print(env.now) - >>> 10.0 - print(fountain.vessel) - >>> 80.0 - - -Location Changing States -======================== - -There are two location changing states, one for Cartesian and one for Geodetic. - -They accept a speed and list of waypoints in their activation. - -.. code-block:: python - - from upstage_des.utils import waypoint_time_and_dist - - class FlatlandCar(UP.Actor): - location: UP.CartesianLocation = UP.CartesianLocationChangingState() - top_speed = UP.State[float](valid_types=float, frozen=True) - - - class Move(UP.Task): - def task(self, *, actor: FlatlandCar): - waypoints = self.get_actor_knowledge(actor, "waypoints", must_exist=True) - time, dist = waypoint_time_and_dist( - start=actor.location, - waypoints=waypoints, - speed=actor.top_speed, - ) - actor.activate_state( - state="location", - speed=actor.top_speed, - waypoints=waypoints, - task=self, - ) - # OR, to get argument hints: - # actor.activate_location_state(...) - yield UP.Wait(time) - actor.deactivate_state( - state="location", - task=self, - ) - - -Then run with: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - car = FlatlandCar( - name="GoDescarte", - location=UP.CartesianLocation(0, 0), - top_speed=5.0, - ) - - task = Move() - waypoints = [ - UP.CartesianLocation(5, 0), - UP.CartesianLocation(5, 5), - ] - car.set_knowledge("waypoints", waypoints) - - task.run(actor=car) - - env.run(until=0.5) - print(car.location) - >>> CartesianLocation(x=2.5, y=0.0, z=0.0) - env.run(until=1.4) - print(car.location) - >>> CartesianLocation(x=5.0, y=1.9999999999999996, z=0.0) - env.run() - print(env.now) - >>> 2.0 - print(car.location) - >>> CartesianLocation(x=5.0, y=5.0, z=0.0) - - -The ``GeodeticLocationChangingState`` works the same way. - - -Creating your own -================= - -To create you own Active State, subclass :py:class:`~upstage_des.states.ActiveState`. - -The bare minimum is to implement the ``_active`` method. - -Here is an example of an ActiveState that changes according to an exponent. - -.. code-block:: python - :linenos: - - from upstage_des.states import ActiveState - from upstage_des.actor import Actor - - class ExponentChangingState(ActiveState): - """A state that changes according to: x_t = x_0 + at^(b)""" - def _active(self, instance: Actor) -> float | None: - """Given a geometric rate change, calculate a new value.""" - data = self.get_activity_data(instance) - now: float = data["now"] - current: float = data["value"] - started: float = data.get("started_at") - if started is None: - return None - starting_value = data.get("starting_value", current) - - a: float = data["a"] - b: float = data["b"] - - t = now - started - to_add = a * (t ** b) - return_value = starting_value + to_add - self.__set__(instance, return_value) - instance._set_active_state_data( - state_name=self.name, - started_at=now if started is None else started, - starting_value = starting_value, - a=a, - b=b, - ) - return return_value - - -There are several particular steps and nuances, so let's go line by line. - -* Line 8: This retrieves activity data stored by your method. - * Part of the data comes from the key/values in ``activate_state`` - * The ``now``, ``value``, and ``started_at`` keys are given to you. - * Everything else is created in this method. -* Line 12: If ``started_at`` is None, it means the state isn't activated - * By returning None, we tell UPSTAGE to just use the last calculated value. - * By default, when an active state is deactivated, it re-calculates its value. -* Line 14: Since this rule depends on initial value plus a time value, get that value as the one we told the state. - * If it's none, it means the state has just been activated (it hasn't been set), so use the current value. -* Line 21: Get the value of the state -* Line 22: Set the value to the state, so if anyone asks for it they can get it. -* line 23-29: This is how we re-inject data back to the next time this method is called. - -The admitted difficulty here is that there's not currently a good way to hint at how to call ``actor.activate_state``. - -Best practice is to document in the docstrings how to call ``activate_state``. UPSTAGE will throw errors if you keyed the kwargs wrong, -but only if you don't use ``data.get()`` for every call. - -Another option is to make a subclass that hints for you: - -.. code-block:: python - - class BetterActor(Actor): - def activate_exponent_state(self, state: str, a: float, b:float, task) -> None: - self.activate_state( - state=state, - a=a, - b=b, - task=task, - ) - - class Changing(BetterActor): - changer: float = ExponentChangingState() - - with UP.EnvironmentContext() as env: - x = Changing(name="example", changer=100) - - # Note that you get useful tab-complete now. - x.activate_exponent_state("changer", 1.0, 2.0, None) - env.run(until=5.0) - # 100 + 1 * 5^2 = 125 - print(x.changer) - >> 125.0 - env.run(until=10.0) - # 100 + 1 * 10^2 = 200 - print(x.changer) - >>> 200.0 - x.deactivate_all_states(task=None) - print(x.changer) - >>> 200.0 - # Now with the state deactivated, we'll re-start the exponential climb. - x.activate_exponent_state("changer", 2.0, 1.0, None) - env.run(until=20.0) - # 200 + 2 * (20 - 10)^1 = 220 - print(x.changer) - >>> 220.0 - - -Note that state activation doesn't require a task. It's just the best place to do it, -because task interrupts automatically deactivate all states. diff --git a/docs/source/user_guide/how_tos/communications.rst b/docs/source/user_guide/how_tos/communications.rst deleted file mode 100644 index c7ea10b..0000000 --- a/docs/source/user_guide/how_tos/communications.rst +++ /dev/null @@ -1,225 +0,0 @@ -============== -Communications -============== - -UPSTAGE provides a built-in classes for passing communications between actors. -There is point-to-point communication which is highly simplified and flexible -for message-passing when you want to abstract away any routing or other concerns. -There is also a communications manager based on routing tables that allows pre- -defined routing to be followed. That same manager is subclassable for more -complicated routing schemes and behaviors. - -Point to Point Communications -============================= - -The :py:class:`~upstage_des.communications.comms.PointToPointCommsManager` class -allows actors to send messages while allowing for simplified retry attempts -and timeouts. It also allows for communications blocking to be turned on -and off on a point to point basis. - -The :py:class:`~upstage_des.communications.comms.Message` class is used to -describe a message, although strings and dictionaries can also be passed as -messages, and UPSTAGE will convert them into the ``Message`` class. The -message will include information about the sender, mode, and other data. - -The communications manager needs to be instantiated and run, and any number -of them can be run to represent different modes of communication. For -simplicity, communications stores on an actor can have multiple modes -to receive communications on. Each mode needs its own manager which can -then determine the right store to send the message to. - -The following code shows how create an actor class that has two communication -interfaces, and then start the necessary comms managers. - -.. code-block:: python - - import upstage_des.api as UP - - class Worker(UP.Actor): - walkie = UP.CommunicationStore(modes=["UHF"]) - intercom = UP.CommunicationStore(modes=["loudspeaker"]) - - - with UP.EnvironmentContext() as env: - w1 = Worker(name="worker1") - w2 = Worker(name="worker2") - - uhf_comms = PointToPointCommsManager(name="Walkies", mode="UHF") - loudspeaker_comms = PointToPointCommsManager(name="Overhead", mode="loudspeaker") - - UP.add_stage_variable("uhf", uhf_comms) - UP.add_stage_variable("loudspeaker", loudspeaker_comms) - - uhf_comms.run() - loudspeaker_comms.run() - -The ``PointToPointCommsManager`` class allows for explicitly connecting actors and the -store that will receive messages, but using the :py:class:`~upstage_des.states.CommunicationStore` -lets the manager auto-discover the proper store for a communications mode, -letting the simulation designer only need to pass the source actor, destination -actor, and message information to the manager. - -To send a message, use the comm manager's ``make_put`` method to return an UPSTAGE -event to yield on to send the message. - -.. code-block:: python - - class Talk(UP.Task): - def task(self, *, actor: Worker): - uhf = self.stage.uhf - friend = self.get_actor_knowledge(actor, "friend", must_exist=True) - msg_evt = uhf.make_put("Hello worker", actor, friend) - yield msg_evt - - - class GetMessage(UP.Task): - def task(self, *, actor: Worker): - get_uhf = UP.Get(actor.walkie) - get_loud = UP.Get(actor.loudspeaker) - - yield UP.Any(get_uhf, get_loud) - - if get_uhf.is_complete(): - msg = get_uhf.get_value() - print(f"{msg.sender} sent '{msg.message}' at {msg.time_sent}") - else: - get_uhf.cancel() - ... - - -Stopping Communications -*********************** - -Communications can be halted for all transmissions of a single manager by setting -``comms_degraded`` to be ``True`` at any time. Setting it back to False will allow -comms to pass again, and any retries that are waiting (and didn't exceed a timeout) -will go through. - -Additionally, specific links can be stopped by adding/removing from ``blocked_links`` -with a tuple of ``(sender_actor, destination_actor)`` links to shut down. The same -timeout rules will apply. There is a ``blocked_nodes`` list that can have single -``Actors`` added to it if you want to block all paths to and from that actor rather -than just one link. - -Routing Table Communications -============================ - -UPSTAGE has a :py:class:`~upstage_des.communications.routing.RoutingTableCommsManager` that -routes comms according to a pre-defined network. Nodes (which are ``Actors``) must be -explicitly connected, and this manager will route through shortest number of hops. - -An example creation of the manager is given below: - -.. code-block:: python - - class CommNode(Actor): - messages = CommunicationStore(modes=None) - - with EnvironmentContext() as env: - nodes = { - name: CommNode(name=name, messages={"modes":["cup-and-string"]}) - for name in "ABCDEFGH" - } - mgr = RoutingTableCommsManager( - name="StaticManager", - mode="cup-and-string", - send_time=1/3600., - retry_max_time=20/3600., - retry_rate=4/3600., - global_ignore=False, - ) - for u, v in ["AB", "BC", "AD", "DE", "EF", "FG", "GH", "HC", "EB"]: - mgr.connect_nodes(nodes[u], nodes[v], two_way=False) - -Note how this manager uses ``connect_nodes()`` to define explicit edges in the -routing graph. You can optionally set ``two_way`` to ``True`` if you want the -edge to go back and forth. - -The manager is still invoked the same by any actor wanting to send a message. -Use ``make_put`` and yield on the returned event to put the message into the -network. - -The reason this is called a routing table method, even though it uses a graph, -is because the underlying ``select_hop`` method only tells the current node -where to send the message once. Once the message is passed along, it'll re-check -what node to go to next. - -This manager allows for degraded comms and comms retry like the point-to-point manager. -If a link is degraded, after the retry fails the network will re-plan a -route assuming the intermediate destination node is no longer available. - -.. note:: - - Message routing does not depend on the Actors used for routing. No messages - are sent to the stores on the Actors connected, and the actors do not need - tasks or processes to handle message routing. The routing manager moves - the messages in time only until it reaches the desired destination. - -The behavior is: - -1. Ask for transmit from SOURCE to DEST -2. Set CURRENT to SOURCE -3. Find the NEXT in the shortest path from CURRENT to DEST -4. If there is no path, stop trying to send and end. -5. Attempt to send to NEXT (this is the degraded comms/retry step) -6. If it can send, do so. Set CURRENT = NEXT. If NEXT is DEST, Goto 8. Otherwise, Goto 3. -7. If it can't send, drop NEXT from the route options. Goto 3 -8. Place message in DEST and end. - -Since this is time-based, a link can re-open during transmission. If the -network has paths: - -:: - - A -> B -> C - A -> D -> E -> F -> G -> H -> C - E -> B -> C - - -and we want to send a message from A to C, but B is blocked, a retry will have the -network eventually take the long way through ADEFGHC. If B comes back online -after the message gets to E, the routing will choose ADEBC instead. - -If B does not come back online, the router will still try to go to B from E -since that is shorter. If B is still down, it will take longer due to the -retry. Set the input ``global_ignore`` to ``True`` to ignore a bad node -for the entire routing and avoid this behavior. - -Stopping Communications -*********************** - -The same two options for stopping a message link exist for this manager. The -``blocked_links`` list and the ``blocked_nodes`` list on the manager can be -updated to prevent comms along a link or to/from a specific node. Note that -if ``global_ignore`` is ``True`` that a blocked link will result in effectively -blocking any comms to the destination node even if you intended only one link -to go down. - -Routing With Multiple Modes -*************************** - -The routing table manager does not allow for hops across different nodes, even -if in practice you could radio someone and that person makes an announcement on -an intercom. This may become a future feature of UPSTAGE. For now, see the section -below for how to make your own router. - -Make Your Own -============= - -The intent of the :py:class:`~upstage_des.communications.routing.RoutingTableCommsManager` class -is to provide an example of a more dynamic comms routing feature. It is based on the -:py:class:`~upstage_des.communications.routing.RoutingCommsManagerBase` class, which holds most -of the work the manager does. This includes managing retries and the behavior steps described -above. The ``RoutingTableCommsManager`` only implements enough features to build, store, and -call the network to determine the next hop. - -To make your own, you only have to implement the ``select_hop`` method, which returns the next -actor to send a message to. Currently, there are not placeholders in the base class for -running other processes (such as acknowledgment, network discovery, etc.) on failures in the -built-in message transmission process. Acknowledgment is implied through the retry features, -but anything more advanced is not built-in. - -It is possible to create a network discovery protocol in effect by creating a process that -determines which links should exist at a given time step. Then, as long as the data structure -you modify there is used in ``select_hop``, you can have more complicated network behaviors -approximated without large amounts of explicit message passing. diff --git a/docs/source/user_guide/how_tos/decision_tasks.rst b/docs/source/user_guide/how_tos/decision_tasks.rst deleted file mode 100644 index 9798cb1..0000000 --- a/docs/source/user_guide/how_tos/decision_tasks.rst +++ /dev/null @@ -1,139 +0,0 @@ -============== -Decision Tasks -============== - -Decision tasks are :py:class:`~upstage_des.task.Task` s that take zero time and were briefly demonstrated in -:doc:`Rehearsal `. The purpose of a Decision task is to allow decision making and -:py:class:`~upstage_des.task_networks.TaskNetwork` routing without moving the simulation clock and do so -inside of a Task Network. - -A decision task must implement two methods: - -* :py:class:`~upstage_des.task.DecisionTask.make_decision` -* :py:class:`~upstage_des.task.DecisionTask.rehearse_decision` - -Neither method outputs anything. The expectation is that inside these methods you modify the task network using: - -* :py:meth:`upstage_des.actor.Actor.clear_task_queue`: Empty a task queue -* :py:meth:`upstage_des.actor.Actor.set_task_queue`: Add tasks to an empty queue (by string name) - you must empty the queue first. -* :py:meth:`upstage_des.actor.Actor.set_knowledge`: Modify knowledge - -The difference between making and rehearsing the decision is covered in the tutorial. The former method -is called during normal operations of UPSTAGE, and the latter is called during a -rehearsal of the task or network. It is up the user to ensure that no side-effects occur during the -rehearsal that would touch non-rehearsing state, actors, or other data. - -There is one class variable that can be set on each subclass of the decision task, which is ``DO_NOT_HOLD``: - -.. code-block:: python - - class Thinker(UP.DecisionTask): - DO_NOT_HOLD = True # default is False - def make_decision(): ... - -This feature lets the user turn off zero time holding on decision tasks, which causes decision -tasks to move right into the next task without allowing anything else to run. For examples and -reasoning, see the following section. - -This feature is most applicable for avoiding race conditions in decision making where a -follow-on task can alter the simulation with its first yield such that other decisions -make would become incorrect [#f1]_. This would only occur for equally-timed decision processes, -and only for the first yield in a Task that follows the decision. For example, deciding -which ``Store`` to queue on may result in an Actor waiting when no wait was expected. - -Zero Time Considerations ------------------------- - -Decision tasks are meant to not advance the clock, but they do cause a zero-time timeout to be -created. This is done to provide other events in the queue the chance to complete at the same -time step before the task network proceeds for the current actor. - -Here is a short example of the default behavior: - -.. code-block:: python - - import upstage_des.api as UP - - class Waiter(UP.Task): - def task(self, *, actor): - print(f"{self.env.now:.1f} >> {actor.name} in Waiter") - yield UP.Wait(1.0) - - class Runner(UP.Task): - def task(self, *, actor): - print(f"{self.env.now:.1f} >> {actor.name} in Runner") - yield UP.Wait(2.0) - - class Thinker(UP.DecisionTask): - def make_decision(self, *, actor): - print(f"{self.env.now:.1f} >> {actor.name} in Thinker") - if "one" in actor.name: - self.set_actor_task_queue(actor, ["Waiter"]) - else: - self.set_actor_task_queue(actor, ["Runner"]) - - net = UP.TaskNetworkFactory( - name="Example Net", - task_classes={"Waiter": Waiter, "Runner":Runner, "Thinker":Thinker}, - task_links={ - "Waiter":UP.TaskLinks(default="Thinker", allowed=["Thinker"]), - "Thinker":UP.TaskLinks(default="", allowed=["Waiter", "Runner"]), - "Runner":UP.TaskLinks(default="Thinker", allowed=["Thinker"]), - }, - ) - - with UP.EnvironmentContext() as env: - a = UP.Actor(name="Actor one", debug_log=True) - b = UP.Actor(name="Actor two", debug_log=True) - - for actor in [a,b]: - n = net.make_network() - actor.add_task_network(n) - actor.start_network_loop(n.name, "Waiter") - - env.run(until=2) - -The result is: - -.. code-block:: python - - >>> 0.0 >> Actor one in Waiter - >>> 0.0 >> Actor two in Waiter - >>> 1.0 >> Actor one in Thinker - >>> 1.0 >> Actor two in Thinker - >>> 1.0 >> Actor one in Waiter - >>> 1.0 >> Actor two in Runner - -Even though ``Actor one`` gets to the decision task first, the internal timeout -preserves ordering of the stops. This would happen even if there was no timeout, -because UPSTAGE yields on the decision task as a simpy process. - -If we were to skip yielding on the process of a ``DecisionTask``, then this ordering -of output would result: - -.. code-block:: python - - ... - # The only modification is to add DO_NOT_HOLD = True - class Thinker(UP.DecisionTask): - DO_NOT_HOLD = True - def make_decision(self, *, actor): - ... - - >>> 0.0 >> Actor one in Waiter - >>> 0.0 >> Actor two in Waiter - >>> 1.0 >> Actor one in Thinker - >>> 1.0 >> Actor one in Waiter - >>> 1.0 >> Actor two in Thinker - >>> 1.0 >> Actor two in Runner - -Note that ``Actor one`` starts the ``Waiter`` task (and stops at the first yield inside) -before ``Actor two`` gets to its decision task. - -Turning off the hold using ``DO_NOT_HOLD = True`` gives a guarantee to ``Actor two`` that -the simulation they see in ``Thinker`` is what they will encounter in the first yield in -``Runner``. - - -.. [#f1] Needing this feature may be a code smell, depending on the situation. Take care - to check that other ways of deciding and queueing might be better suited. diff --git a/docs/source/user_guide/how_tos/entity_naming.rst b/docs/source/user_guide/how_tos/entity_naming.rst deleted file mode 100644 index 8caf75c..0000000 --- a/docs/source/user_guide/how_tos/entity_naming.rst +++ /dev/null @@ -1,128 +0,0 @@ -============== -Named Entities -============== - -Named entities are an :py:class:`~upstage_des.base.EnvironmentContext` and -:py:class:`~upstage_des.base.NamedUpstageEntity` enabled feature where you -can store instances in particular "entity groups" to gather them from later. -UPSTAGE's :py:class:`~upstage_des.actor.Actor` inherits from :py:class:`~upstage_des.base.NamedUpstageEntity`, -giving all Actors the feature. Similarly, the ``SelfMonitoring<>`` resources -do the same to enable quick access to recorded simulation data. - -All Actors are retrievable with the :py:meth:`~upstage_des.base.UpstageBase.get_actors` -method if they inherit from Actor. - -Entities are retrievable with :py:meth:`~upstage_des.base.UpstageBase.get_all_entity_groups` -and :py:meth:`~upstage_des.base.UpstageBase.get_entity_group`. - -Defining a named entity is done in the class definition: - -.. code-block:: python - - class Car(UP.Actor, entity_groups=["vehicle"]): - ... - - class Talker(UP.Actor, entity_groups=["has-radio"]): - ... - - class Plane(UP.Actor, entity_groups=["vehicle", "air"]): - ... - - class FastTalkingPlane(Plane, Talker): - ... - - class NoSpecificGroup(UP.Actor): - ... - - class Different(UP.NamedUpstageEntity, entity_groups=["separate"]): - ... - - -Once you are in an environment context you can get the actual instances. - -.. code-block:: python - - with UP.EnvironmentContext(): - manage = UP.UpstageBase() - - c1 = Car(name="car1") - c2 = Car(name="car2") - p = Plane(name="plane") - fp = FastTalkingPlane(name="fast plane") - other = NoSpecificGroup(name="all alone") - talk = Talker(name="Basic Talker") - d = Different() - - actor_entities = manage.get_actors() - print(actor_entities) - >>> [Car: car1, Car: car2, Plane: plane, FastTalkingPlane: fast plane, NoSpecificGroup: all alone, Talker: Basic Talker] - - vehicles = manage.get_entity_group("vehicle") - print(vehicles) - >>> [Car: car1, Car: car2, Plane: plane, FastTalkingPlane: fast plane] - - air = manage.get_entity_group("air") - print(air) - >>> [Plane: plane, FastTalkingPlane: fast plane] - - radio = manage.get_entity_group("has-radio") - print(radio) - >>> [FastTalkingPlane: fast plane, Talker: Basic Talker] - - different = manage.get_entity_group("separate") - print(different) - >>> [<__main__.Different object at ...>] - -Note that entity groups are inheritable and that you can inherit from ``NamedUpstageEntity`` -and retrieve the instance without needing an Actor. You may also create an instance of -``UpstageBase`` to get access to the required methods. Actors and Tasks can access -that method already. - -If you are going to create a non-Actor version of a named entity, ensure your init -calls ``super()``. The following example shows a use case where a simulation may -want to look up entities in the simulation universe that don't need to be actors. - -.. code-block:: python - - class PowerGenerator(UP.NamedUpstageEntity): - def __init__(self, name: str, kwh: float) -> None: - super().__init__() - self.name = name - self.kwh = kwh - - def __repr__(self) -> str: - return f"{self.name} - {self.kwh}KwH" - - - class Nuclear(PowerGenerator, entity_groups="Nuclear"): - def __init__(self, name: str, kwh: float, num_towers: int) -> None: - super().__init__(name, kwh) - self.num_towers = num_towers - - - class Planner(UP.DecisionTask): - def make_decision(self, *, actor: UP.Actor) -> None: - # Decide on which power plant to upgrade next - plants = self.get_entity_group("PowerGenerator") - nuclear = self.get_entity_group("Nuclear") - print(plants) - print(nuclear) - - - with UP.EnvironmentContext(random_seed=321456) as env: - rng = UP.get_stage().random - pgs = [ - PowerGenerator(f"{i}", rng.randint(10, 100)) - for i in range(5) - ] - pgs += [ - Nuclear(f"Nuc_{i}", rng.randint(10, 100), rng.randint(1,4)) - for i in range(5) - ] - # You wouldn't actually make an actor just to run the task, - # but it serves as a reminder of getting entities inside - # an UpstageBase subclass - t = Planner() - t.make_decision(actor=UP.Actor(name="example")) - >>>[0 - 61KwH, 1 - 71KwH, 2 - 58KwH, 3 - 43KwH, 4 - 37KwH, Nuc_0 - 66KwH, Nuc_1 - 60KwH, Nuc_2 - 37KwH, Nuc_3 - 53KwH, Nuc_4 - 15KwH] - >>>[Nuc_0 - 66KwH, Nuc_1 - 60KwH, Nuc_2 - 37KwH, Nuc_3 - 53KwH, Nuc_4 - 15KwH] diff --git a/docs/source/user_guide/how_tos/environment.rst b/docs/source/user_guide/how_tos/environment.rst deleted file mode 100644 index 889c65e..0000000 --- a/docs/source/user_guide/how_tos/environment.rst +++ /dev/null @@ -1,53 +0,0 @@ -=================== -Environment Context -=================== - -UPSTAGE uses Python's [context variable](https://docs.python.org/3/library/contextvars.html) -capabilities to safely manage "global" state information while not polluting the module -level data with run-specific information. - -The context manager accepts three arguments: - -1. Simulation start time (passes through to ``simpy.Environment``) -2. A random seed for ``random.Random`` -3. A random number generator object, if different than ``random.Random`` - -For more about the random numbers, see :doc:`Random Numbers `. - -.. note:: - - If you get a warning or error about not finding an environment, you have likely - tried to instantiate an actor, task, or other UPSTAGE object outside of an - environment context. - - -Creating Contexts -================= - -Use the ``EnvironmentContext`` context manager: - -.. code:: python - - impoprt upstage_des.api as UP - - with UP.EnvironmentContext() as env: - ... - # everything in here can find that environment - ... - env.run() - -Or, create a context at the current scope: - -.. code:: python - - from upstage_des.base import create_top_context, clear_top_context - - ctx = create_top_context() - env = ctx.env_ctx.get() - ... - env.run() - - clear_top_context(ctx) - -This way is friendlier to Jupyter notebooks, where you might run a simulation and want to -explore the data without needing to remain in the context manager. diff --git a/docs/source/user_guide/how_tos/events.rst b/docs/source/user_guide/how_tos/events.rst deleted file mode 100644 index a77dc38..0000000 --- a/docs/source/user_guide/how_tos/events.rst +++ /dev/null @@ -1,157 +0,0 @@ -====== -Events -====== - -UPSTAGE Events mimic the SimPy events, and provide features that enable interrupts, rehearsal and other features with Task Networks. - -All events accept a ``rehearsal_time_to_complete`` argument. - -The available UPSTAGE events are: - -:py:class:`~upstage_des.events.Event` -------------------------------------- - -Mimics SimPy's raw ``Event``, useful for marking pauses until a success. - -See :py:meth:`~upstage_des.actor.Actor.create_knowledge_event` for a use case. - -One use case is the knowledge event, which enables a way to publish and event to an actor, and have some other source ``succeed`` it. - -.. code-block:: python - - class ActorTask(UP.Task): - def task(self, *, actor): - event = actor.create_knowledge_event(name="pause") - yield event - - - class ManagerTask(UP.Task): - def task(self, *, actor): - subordinate: UP.Actor = actor.subordinates[0] - subordinate.succeed_knowledge_event(name="pause", some_data={...}) - -The Event also has a "payload", which is created from keyword arguments to the :py:meth:`~upstage_des.evetns.Event.succeed` method. -The payload can be retrieved using :py:meth:`~upstage_des.evetns.Event.get_payload`. - - -:py:class:`~upstage_des.events.Wait` ------------------------------------- - -A standard SimPy timeout. Can be explicit or generate from a random uniform distribution. - -The random uniform distribution accepts an input for the rehearsal time, while the base version rehearses at the given time. - -.. code-block:: python - - yield UP.Wait(3.14) - ... - yield UP.Wait.from_random_uniform(low, high, rehearsal_time_to_complete=high) - - -:py:class:`~upstage_des.events.Get` ------------------------------------ - -Get from a store or container. - -.. code-block:: python - - get_event = UP.Get(some_store) - item = yield get_event - assert item == get_event.get_value() - - amount = 12.3 - get_event = UP.Get(some_container, amount) - yield get_event - assert get_event.get_value() == amount - - -:py:class:`~upstage_des.events.FilterGet` ------------------------------------------ - -A get with a filter function, used for SimPy's ``FilterStore``. - -.. code-block:: python - - get_event = UP.FilterGet(some_store, filter=lambda item: item.value > 10) - item = yield get_event - - -:py:class:`~upstage_des.resources.sorted.SortedFilterGet` ---------------------------------------------------------- - -A get with a filter or sorting function, used with :py:class:`~upstage_des.resources.sorted.SortedFilterStore`, and others. - -.. code-block:: python - - get_event = UP.SortedFilterGet( - some_store, - filter=lambda item: item.value > 10, - sorter=lambda item: (item.property, item.other_property), - ) - item = yield get_event - - -:py:class:`~upstage_des.events.Put` ------------------------------------ - -Put something into a store or container - -.. code-block:: python - - item = [1,2,3.4] - put_event = UP.Put(some_store, item) - yield put_event - assert item in some_store.items - - amount = 12.3 - yield UP.Put(some_store, amount) - - -:py:class:`~upstage_des.events.ResourceHold` --------------------------------------------- - -Put and release holds on limited resources. - -.. code-block:: python - - a_resource = SIM.Resource(env, capacity=1) - request_object = UP.ResourceHold(a_resource) - yield request_object - # Now you have a hold on the resource - ... - yield request_object - # Now you've given it back - - -:py:class:`~upstage_des.events.All` ------------------------------------ - -Succeed when all passed events succeed. - -.. code-block:: python - - get_event = UP.Get(some_store) - wait_event = UP.Wait(3.14) - yield Any(get_event, wait_event) - - assert get_event.is_complete() - assert wait_event.is_complete() - - -:py:class:`~upstage_des.events.Any` ------------------------------------ - -Succeed when any passed events succeed - -.. code-block:: python - - get_event = UP.Get(some_store) - wait_event = UP.Wait(3.14) - yield Any(get_event, wait_event) - - # Determine what passed - if get_event.is_complete(): - item = get_event.get_value() - else: - # cancel the get or else it will succeed - get_event.cancel() diff --git a/docs/source/user_guide/how_tos/geography.rst b/docs/source/user_guide/how_tos/geography.rst deleted file mode 100644 index 570b804..0000000 --- a/docs/source/user_guide/how_tos/geography.rst +++ /dev/null @@ -1,254 +0,0 @@ -========= -Geography -========= - -UPSTAGE has built-in features for simple geographic math and behaviors. These features are built up into a :py:class:`~upstage_des.states.GeodeticLocation` state. - -Discrete Event Simulation does not lend itself well to geography and repeated distance checking (for something like a sensor, e.g.), so UPSTAGE provides the capability to -schedule intersections of moving actors and stationary sensors. Those features are covered in the :doc:`Motion Manager ` documentation. - -UPSTAGE prefers geography to be in Latitude / Longitude / Altitude order. - -The geographic code is not meant to maintain a high level of precision in all calculations. Given the naturally abstracting nature of DES, some small errors are expected in terms of timing and distances. -For most simulations, these small differences have no effect on the results or behavior. Also, the code is done entirely with built-in ``math`` functions. We preferred to avoid ``numpy`` just to keep -the install dependencies to ``SimPy``. - - -Geographic Data Types and State -=============================== - -These are the built-in features that use geography: - -:py:class:`~upstage_des.data_types.GeodeticLocation` ----------------------------------------------------- - -This data type stores a Latitude / Longitude / Altitude (optional) for a point around the globe. - -Two geodetic locations can be subtracted from each other to get their great circle path distance at zero altitude: - -.. note:: - - All distances are ground distances for Spherical and WGS84 unless you ask for a different one. Therefore, all speeds should be ground speed. - - Altitude is taken into account for intersection and range (see :doc:`motion_manager`) - -.. code-block:: python - - from upstage_des.geography import Spherical - - with UP.EnvironmentContext(): - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("altitude_units", "ft") - # Note that in_radians defaults to False. - loc1 = UP.GeodeticLocation(33.7490, -84.3880, 1050, in_radians=False) - loc2 = UP.GeodeticLocation(39.7392, -104.9903, 30_000, in_radians=False) - dist = loc1 - loc2 - print(dist) - >>> 1052.6666594454714 - dist_with_altitude = loc1.dist_with_altitude(loc2) - >>> 1052.6774420025818 - straight_line = loc1.straight_line_distance(loc2) - >>> 1049.3621152419862 - -Location instances *must* be created within an :py:class:`~upstage_des.base.EnvironmentContext` context, otherwise they won't have access to the geographic model at runtime. Additionally, these stage variables must be set: - -* ``stage_model`` must be set to be either ``Spherical`` or ``WGS84``, or whatever class performs ``.distance`` on lat/lon/altitude pairs. -* ``distance_units``: One of: "m", "km", "mi", "nmi", or "ft" -* OPTIONAL: ``altitude_units``: One of: "m", "km", "mi", "nmi", or "ft". Altitude can be different, because feet or meters are typical for altitude, while mi/nmi/km are typical for point to point distances. - - * Include this for any geographic features using intersections or for the ``GeodeticLocationChangingState`` - -See the bottom of this page for a method to store geographic data conveniently without needing an environment context. - -The distance between two points is the great circle distance, and altitude is ignored. This is a design choice for simplicity, where altitude change doesn't affect timing, especially over long distances. This also means -that any speeds you specify are implicitly ground speed, which is more useful. - -However, if a sensor is looking straight up, the distance to an object 30 thousand feet up shouldn't be zero. To account for altitude in the distance, use -:py:func:`~upstage_des.data_types.GeodeticLocation.dist_with_altitude` or :py:func:`~upstage_des.data_types.GeodeticLocation.straight_line_distance`. -Note that the intersection models (covered elsewhere) do distance checks in ECEF, not with the ``GeodeticLocation`` subtraction method, so you don't have to worry about this distinction for those motion features. - -Once a ``GeodeticLocation`` is created, it cannot be changed. This is for safety of not changing a location from underneath code that expects to use it a certain way. Some methods are provided to help get copies: - -* :py:meth:`~upstage_des.data_types.GeodeticLocation.copy`: Make a copy of the location -* :py:meth:`~upstage_des.data_types.GeodeticLocation.to_radians`: Make a copy of the location with the latitude and longitude in radians -* :py:meth:`~upstage_des.data_types.GeodeticLocation.to_degrees`: Make a copy of the location with the latitude and longitude in degrees - - -For comparison, here's what ``pyproj`` gets for the calculations (pyproj is not currently a dependency for UPSTAGE): - -.. code-block:: python - - import pyproj - from upstage_des.api import unit_convert - # NOTE: Numpy is not a requirement of UPSTAGE - import numpy as np - - lonlatalt_to_ecef_transformer: pyproj.Transformer = pyproj.Transformer.from_crs( - {"proj": "latlong", "ellps": "WGS84", "datum": "WGS84"}, - {"proj": "geocent", "ellps": "WGS84", "datum": "WGS84"} - ) - lat1, lon1 = 33.7490, -84.3880 - lat2, lon2 = 39.7392, -104.9903 - ecef_1 = lonlatalt_to_ecef_transformer.transform(lon1, lat1, 0) - ecef_2 = lonlatalt_to_ecef_transformer.transform(lon2, lat2, 0) - dist_m = np.sqrt(((np.array(ecef_1) - np.array(ecef_2))**2).sum()) - dist = unit_convert(dist_m, "m", "nmi") - # The straight-line ECEF distance - print(dist) - >>> 1049.302887568968 - az12,az21,dist = pyproj.Geod(ellps='WGS84').inv(-84.3880, 33.7490, -104.9903, 39.7392) - dist = UP.unit_convert(dist, "m", "nmi") - # The great-circle distance - print(dist) - >>> 1053.3987119745102 - -Both distances are within .07% of UPSTAGE's calculations. - - - -:py:class:`~upstage_des.states.GeodeticLocationChangingState` -------------------------------------------------------------- - -This is a State that allows activation and movement along great-circle waypoints with altitude changing along the -waypoints. When initializing, it accepts a ``GeodeticLocation`` object, and it returns those when you ask it for -the state's value. Here is its basic usage: - -.. code-block:: python - - from upstage_des.utils import waypoint_time_and_dist - - class Plane(UP.Actor): - location = UP.GeodeticLocationChangingState(recording=True) - speed = UP.State[float](valid_types=float, default=100.0) - - class Fly(UP.Task): - def task(self, *, actor: Plane): - # waypoints do not include the starting point - waypoints = actor.get_knowledge("flying to", must_exist=True) - time, dist = waypoint_time_and_dist( - start=actor.location, - waypoints=waypoints, - speed=actor.speed, - ) - actor.activate_location_state( - state="location", - waypoints=waypoints, - speed=actor.speed, - task=self, - ) - yield UP.Wait(time) - actor.deactivate_state(state="location", task=self) - - - with UP.EnvironmentContext(): - plane = Plane( - name="Flyer", - location = UP.GeodeticLocation(lat, lon, alt), - ) - ... - -The :py:func:`~upstage_des.utils.waypoint_time_and_dist` function is a convenience function that gets the great -circle distance and time over a set of waypoints to help schedule the arrival time. - - -Cartesian Locations -=================== - -These aren't geographic, but they serve the same purpose, so we include them here. - -:py:class:`~upstage_des.data_types.CartesianLocation` ------------------------------------------------------ - -This data type stores an X / Y / Z (optional) location in 2 or 3D space (z is set to zero if not included). - -Two cartestian locations can be subtracted from each other to get their distance: - -.. code-block:: python - - with UP.EnvironmentContext(): - # use_altitude_units defaults to False - meaning you don't need to set the stage variables. - loc1 = UP.CartesianLocation(33.7490, -84.3880, 1050, use_altitude_units=False) - loc2 = UP.CartesianLocation(39.7392, -104.9903, 30_000, use_altitude_units=False) - dist = loc1 - loc2 - print(dist) - >>> 28950.007950556097 - - -We still allow you to set distance and altitude units because the 'z' value could be in a different units system. - -.. code-block:: python - - with UP.EnvirronmentContext(): - UP.add_stage_variable("distance_units", "km") - UP.add_stage_variable("altitude_units", "m") - loc1 = UP.CartesianLocation(33.7490, -84.3880, 1050, use_altitude_units=True) - loc2 = UP.CartesianLocation(39.7392, -104.9903, 30_000, use_altitude_units=True) - dist = loc1 - loc2 - print(dist) - >>> 36.0338696413527 - -The distance is always implied to be in ``distance_units``, without setting it. If the z component is in a different unit, then we need to know both to get the straight-line distance. - - -:py:class:`~upstage_des.states.CartesianLocationChangingState` --------------------------------------------------------------- - -This active state works the exact same as the ``GeodeticLocationChangingState`` , except that it requires -waypoints to be ``CartesianLocation`` s. - - -Geography Sub-Module -==================== - -The :py:mod:`upstage_des.geography` module contains: - -:py:class:`~upstage_des.geography.spherical.Spherical` ------------------------------------------------------- - -This class contains methods for finding distances, positions, and for segmenting great-circle paths on the assumption of a spherical earth. - -Typically, you will not need to use these methods directly, but they are avaiable and can be useful for results plotting, for example. - -The most useful methods, besides distance, may be: - -#. :py:meth:`~upstage_des.geography.spherical.Spherical.geo_linspace`, which will give you evenly spaced points along a great circle route. -#. :py:meth:`~upstage_des.geography.spherical.Spherical.geo_circle`, which will give you evently spaced points to draw a circle in spherical coordinates -#. :py:meth:`~upstage_des.geography.spherical.Spherical.point_from_bearing_dist`, which gives you a point relative to a base location at some distance and bearing. - -:py:class:`~upstage_des.geography.wgs84.WGS84` ----------------------------------------------- - -This class contains methods for finding distances, positions, and for segmenting great-circle paths on the assumption of a WGS84 ellipsoid. These methods take longer to run than the Spherical version, -so be sure the extra accuracy is worth it. - -Typically, you will not need to use these methods directly, but they are avaiable and can be useful for results plotting, for example. - -The most useful methods, besides distance, may be: - -#. :py:meth:`~upstage_des.geography.spherical.WGS84.geo_linspace`, which will give you evenly spaced points along a great circle route. -#. :py:meth:`~upstage_des.geography.spherical.WGS84.geo_circle`, which will give you evently spaced points to draw a circle in spherical coordinates -#. :py:meth:`~upstage_des.geography.spherical.WGS84.point_from_bearing_dist`, which gives you a point relative to a base location at some distance and bearing. - -:py:mod:`upstage_des.geography.intersections` ---------------------------------------------- - -The :py:func:`~upstage_des.geography.intersections.get_intersection_locations` function calculates an intersection between a great circle path and a sphere. It can be passed an instance of ``Spherical`` or ``WGS84`` -to do distance calculations with. - -The intersections are calculated by taking evenly spaced points along the great circle path and finding the two points where an intersection occurs between. It then divides that segment more finely, and calculates -the two points where the intersection is between. The number of point in the subdividing process is an input through ``subdivide_levels``, which default to 10 and 20. Before the subdivision happens, the code uses -``dist_between`` to do the first division. The default is 5 nautical miles. If you have a 5 nmi distance, then do 10 and 20 subdivisions, the distance of each segment is roughly 152 feet, which is the maximum error -of the intersection point in that case. - - -Storing Geographic Data -======================= - -While the storage and instantiation of geographic objects is mostly within your control, the main caveat is that a ``GeodeticLocation`` requires the stage to exist. -This means that you can only create a ``GeodeticLocation`` within an ``EnvironmentContext``. - -To store data in an easily passable format, UPSTAGE has a :py:class:`~upstage_des.data_types.GeodeticLocationData` class. - -This class instantiates with the same inputs as the ``GeodeticLocation``, and has a single method: ``make_location()``. That method generates the ``GeodeticLocation``, -letting you pass around the data object until you're ready for it inside an environment context. diff --git a/docs/source/user_guide/how_tos/keyvalue_states.rst b/docs/source/user_guide/how_tos/keyvalue_states.rst deleted file mode 100644 index 38d2bd7..0000000 --- a/docs/source/user_guide/how_tos/keyvalue_states.rst +++ /dev/null @@ -1,227 +0,0 @@ -================ -Key/Value States -================ - -Key/Value states are a particular :doc:`State ` that acts like -a standard python dictionary or dataclass. - -There is a use case for defining at runtime the names and values of Actor states, -but this is not supported through the class definition syntax. The -:py:class:`~upstage_des.states.DictionaryState` solves this problem by allowing -a runtime ingest of a dictionary of arbitrary keys and values. The other state, -:py:class:`~upstage_des.states.DataclassState` lets you supply a dataclass type -to the state. - -These state types allow recording of entries or attributes of the state, while -most other states cannot record on internal changes. See -:ref:`Complex States ` for more information about that issue. - -In the case of a dictionary, you can type hint the values (the keys must be -strings). The dataclass types will also be known, and both will be checked -during runtime whenever you set an attribute or entry. - -.. note:: - - These states record data as ``statename.attribute``, but do not go - deeper into sub-attributes when recording. They work best when the - values are not themselves key/value-like objects. - -To summarize, the main reasons for using these states are: - -1. Type checking attributes or values during runtime -3. Direct recording of attributes on a per-attribute basis -4. Runtime creation of recordable states - -DictionaryState -############### - -A dictionary state is created in the same way as other states, and works with -:doc:`data recording `. Note that the data recording functions -will be given the entire dictionary for the state, not just the single entry -being recorded. - -.. code-block:: python - - import upstage_des.api as UP - - def total_recorder(time: float, value: dict[str, int]) -> int: - return sum(value.values()) - - class Usher(UP.Actor): - people_seen = UP.DictionaryState[int]( - recording=True, - recording_functions=[(total_recorder, "total_customers")], - ) - - class TicketTaking(UP.Task): - def task(self, *, actor: Usher): - for customer in ["adult", "adult", "child", "adult", "child"]: - actor.people_seen[customer] += 1 - yield UP.Wait(0.1) - -Then it can be instantiated and run: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - ush = User(name="Ticketeer", people_seen={"adult": 0, "child": 0}) - TicketTaking().run(actor=ush) - env.run() - print(env.now) - >>> 0.5 - print(ush._state_histories) - >>> { - >>> 'people_seen.adult': [(0.0, 0), (0.0, 1), (0.1, 2), (0.3, 3)], - >>> 'total_customers': [(0.0, 0), (0.0, 1), (0.1, 2), (0.2, 3), (0.3, 4), (0.4, 5)], - >>> 'people_seen.child': [(0.0, 0), (0.2, 1), (0.4, 2)] - >>> } - - -Dictionary states allow you to add more entries to the dictionary explicitly, -but they will behave like regular dictionaries in that unseen keys will cause -errors. You can mitigate this in the usual way: - -.. code-block:: python - - class TicketTaking(UP.Task): - def task(self, *, actor: Usher): - for customer in ["adult", "adult", "child", "adult", "child", "vip"]: - curr = actor.people_seen.setdefault(customer, 0) - actor.people_seen[customer] += 1 - yield UP.Wait(0.1) - -When you create a data table from the sim, the results come out naturally with -the pattern of ``.`` and the value you assigned. If you used -complicated objects as values in the dictionary, those will be processed as they -would be in any other circumstance. Note that the following example would fail -a ``mypy`` check since it can't interpret ``data["other"]``. - -.. code-block:: python - - from upstage_des.data_utils import create_table - - class Usher(UP.Actor): - data = UP.DictionaryState[dict](recording=True) - tracker = UP.DictionaryState[int](recording=True) - - with UP.EnvironmentContext(): - ush = Usher(name="Ticketeer", data={"group": {}, "other": 3}, tracker={"value": 1}) - ush.data["group"]["this_key"] = 1 - ush.data["other"] += 1 - ush.data["group"]["new_key"] = {"another": "dictionary"} - ush.tracker["value"] += 1 - - rows, cols = create_table() - print(rows) - >>> ('Ticketeer', 'Usher', 'data.group.this_key', 0.0, 1, None) - >>> ('Ticketeer', 'Usher', 'data.group.new_key', 0.0, {'another': 'dictionary'}, None) - >>> ('Ticketeer', 'Usher', 'data.other', 0.0, 3, None) - >>> ('Ticketeer', 'Usher', 'data.other', 0.0, 4, None) - >>> ('Ticketeer', 'Usher', 'tracker.value', 0.0, 1, None) - >>> ('Ticketeer', 'Usher', 'tracker.value', 0.0, 2, None) - -The ``create_table()`` function will also recognize the ``DictionaryState`` if it -``save_static=True`` and output any non-recorded values in the same format. - -DataclassState -############## - -A dataclass state is created in the same way as other states, and works with -:doc:`data recording `. Note that the data recording functions -will be given the entire dataclass for the state, not just the single attribute -being updated. - -The following example shows how to use, type hint, and examine a dataclass state. - -.. code-block:: python - - from dataclasses import dataclass, fields - import upstage_des.api as UP - from upstage_des.type_help import TASK_GEN - - @dataclass - class TestDC: - a: int - b: float - - def recorder(time: float, value: TestDC) -> float: - return value.a + value.b - - class ExampleActor(UP.Actor): - dc_state = UP.DataclassState[TestDC]( - valid_types=TestDC, - recording=True, - recording_functions=[(recorder, "total_of_data")], - ) - - class SomeTask(UP.Task): - def task(self, *, actor: ExampleActor) -> TASK_GEN: - actor.dc_state.a += 1 - actor.dc_state.b += 4 - yield UP.Wait(0.1) - actor.dc_state.b += 4 - yield UP.Wait(0.1) - actor.dc_state.a -= 3 - - with UP.EnvironmentContext() as env: - ea = ExampleActor(name="Exam", dc_state=TestDC(0, 0.0)) - task = SomeTask() - task.run(actor=ea) - env.run() - # fields() works: - fs = fields(ea.dc_state) - assert [f.name for f in fs] == ["a", "b"] - - # This will error - ea.dc_state.a = "cause error" - - # let's check histories - assert len(ea._state_histories) == 3 - assert ea._state_histories["dc_state.a"] == [(0.0, 0), (0.0, 1), (0.2, -2)] - assert ea._state_histories["dc_state.b"] == [(0.0, 0.0), (0.0, 4.0), (0.1, 8.0)] - assert ea._state_histories["total_of_data"] == [ - (0.0, 0.0), - (0.0, 1.0), - (0.0, 5.0), - (0.1, 9.0), - (0.2, 6.0), - ] - -Type Hinting -############ - -Dictionary states, if untyped, allow for any kind of value. If you define -``valid_types`` the state will check that any input to a dictionary value -matches one of those types. The dictionary state does not do per-key typing. This -means you will need to check if the types vary in how they can be operated on. - -.. warning:: - - For stability, UPSTAGE assumes all ``DictionaryState`` dictionaries only - have strings for keys. - -As usual, make sure the type hint for the state matches valid_types so that your -static type checker and the internal state type checking match. - -.. code-block:: python - - import upstage_des.api as UP - - class Usher(UP.Actor): - people_seen = UP.DictionaryState[int | float](valid_types=(int, float)) - - with UP.EnvironmentContext(): - ush = User(name="Ticketeer", people_seen={"Customer": 1.0}) - ush.people_seen["boss"] = 1 - # This will error - ush.people_seen["boss"] = "Boss' Name" - -Dataclasses will also type check using the ``__annotations__`` information -from the dataclass object. For dataclasses, supply the class object to ``valid_types`` -to enable that feature. - -.. warning:: - - The runtime type checking of these values does not recurse. Keeping the - dictionary value types simple: ``valid_types = (int, str, dict)``, e.g. - is diff --git a/docs/source/user_guide/how_tos/knowledge.rst b/docs/source/user_guide/how_tos/knowledge.rst deleted file mode 100644 index d80925b..0000000 --- a/docs/source/user_guide/how_tos/knowledge.rst +++ /dev/null @@ -1,140 +0,0 @@ -========= -Knowledge -========= - -Knowledge is a property of an :py:meth:`~upstage_des.actor.Actor` that is intended to be a -temporary space for storing information about the Actor's goals or perception. While many -actions that use knowledge could be accomplished with :doc:`States `, knowledge is -created separately to include other checks and debug logging support. - -While you can use knowledge for anything you want, a typical pattern is to use knowledge to support task -network flow. A knowledge entry could be a list of activities to do. A :py:class:`~upstage_des.task.DecisionTask` could -pop entries from a knowledge list and re-plan the network. - -Knowledge is also used to store events that are known only to an Actor to support some process -continuation patterns, described farther below. - -Knowledge is accessed and updated through: - -* In ``Actor`` instantiation - - * Use the ``initial_knowledge`` keyword argument when creating your actor. - - * Pass a dictionary to set as knowledge. - -* :py:meth:`upstage_des.actor.Actor.get_knowledge` - - * ``name``: The name of the knowledge. - - * ``must_exist``: Boolean for raising an exception if the knowledge does not exist. - -* :py:meth:`upstage_des.task.Task.get_actor_knowledge` - - * ``actor``: The actor that has the knowledge. - - * ``name`` and ``must_exist``, as above. - -* :py:meth:`upstage_des.actor.Actor.set_knowledge` - - * ``name``: The name of the knowledge to set. - - * ``value``: Any object to set as the value. - - * ``overwrite``: Boolean for allowing an existing value to be changed. Defaults to False, and will raise an exception if not allowed to overwrite. - - * ``caller``: Optional information - through a string - of who is calling the knowledge set method. This records to the actor debug log, if enabled. - -* :py:meth:`upstage_des.task.Task.set_actor_knowledge` - - * ``actor``: The actor that you want to set knowledge on. - - * All other inputs as above, except that ``caller`` is filled out for you. - -* :py:meth:`upstage_des.actor.Actor.clear_knowledge` - - * ``name``: The name of the knowledge to delete. - - * ``caller``: Same as above. - -* :py:meth:`upstage_des.task.Task.clear_actor_knowledge` - - * ``actor``: The actor to delete knowledge from. - - * All other inputs as above, except that ``caller`` is filled out for you. - - -The actor knowledge can be set and retrieved from the actor itself, and the ``Task`` convenience methods are there -to provide data to the actor debug log (if ``debug_logging=True`` is set on the Actor) to help trace where an actor's -information came from. - -For convenience, you can get and remove knowledge in one method using: - -* :py:meth:`~upstage_des.actor.Actor.get_and_clear_knowledge` on the Actor. -* :py:meth:`~upstage_des.task.Task.get_and_clear_actor_knowledge` on the Task. - - -Bulk Knowledge --------------- - -All the above methods can be operated on in bulk: - -1. :py:meth:`~upstage_des.actor.Actor.set_bulk_knowledge`: Set knowledge using a dictionary. -2. :py:meth:`~upstage_des.actor.Actor.get_bulk_knowledge`: Get knowledge using an iterable of names. -3. :py:meth:`~upstage_des.actor.Actor.clear_bulk_knowledge`: Clear knowledge using an iterable of names. -4. :py:meth:`~upstage_des.actor.Actor.get_and_clear_bulk_knowledge`: Get a dictionary of knowledge and clear it. - -The tasks contain similarly named methods: - -1. :py:meth:`~upstage_des.task.Task.set_actor_bulk_knowledge` -2. :py:meth:`~upstage_des.task.Task.get_actor_bulk_knowledge` -3. :py:meth:`~upstage_des.task.Task.clear_actor_bulk_knowledge` -4. :py:meth:`~upstage_des.task.Task.get_and_clear_actor_bulk_knowledge` - -This is most useful for initializing or passing large amounts of information to an actor. - - -Knowledge Events ----------------- - -It is often times useful to hold an actor in a task until an event succeeds. UPSTAGE Actors -have a :py:meth:`~upstage_des.actor.Actor.create_knowledge_event` and :py:meth:`~upstage_des.actor.Actor.succeed_knowledge_event` -method to support this activity (also described in :doc:`Events `) - -.. code-block:: python - - HAIRCUT_DONE = "haircut is done" - - class Chair(UP.Actor): - sitting = UP.ResourceState[UP.SelfMonitoringStore]() - - - class Customer(UP.Actor): - hair_length = UP.State[float](recording=True) - - - class Haircut(UP.Task): - def task(self, *, actor: Customer): - assigned_chair = self.get_actor_knowledge( - actor, - name="chair", - must_exist=True, - ) - evt = actor.create_knowledge_event(name=HAIRCUT_DONE) - yield UP.Put(assigned_chair.sitting, actor) - yield evt - print(evt.get_payload()) - - - class DoHaircut(UP.Task): - def task(self, *, actor: Chair): - customer = yield UP.Get(actor.sitting) - yield UP.Wait(30.0) - customer.hair_length *= 0.5 - customer.succeed_knowledge_event(name=HAIRCUT_DONE, data="Have a nice day!") - - -The above simplified example shows how UPSTAGE tasks can work with knowledge events to -support simple releases from other tasks without adding stores or other signaling mechanisms. - -The succeed event method also clears the event from the knowledge. If a task is interrupted -on a knowledge event, the event is cancelled and the knowledge is cleared. diff --git a/docs/source/user_guide/how_tos/mimic_states.rst b/docs/source/user_guide/how_tos/mimic_states.rst deleted file mode 100644 index 1b47adc..0000000 --- a/docs/source/user_guide/how_tos/mimic_states.rst +++ /dev/null @@ -1,157 +0,0 @@ -============ -Mimic States -============ - -Mimic states allow one actor to use another actor's state in place of its own. If you have two "Worker" actors riding in a car, it is -useful to have their location mimic that of the car, rather than write a task for the workers that does the same thing as the car. - -.. note:: - - Mimic states test to see if their types are compatible (if defined through ``valid_types``). If they - are note, an error will be raised. - -Mimic states are activated and deactivated in a similar manner to other states, and they can mimic :doc:`ActiveStates ` as well. - -.. code-block:: python - - ... - actor.activate_mimic_state( - self_state="name of state on self", - mimic_state="name of state to mirror", - mimic_actor=actor_object_that_has_mimic_state, - task=self, - ) - ... - actor.deactivate_mimic_state( - self_state="name of state on self", - task=self, - ) - ... - - -Here is a complete example that demonstrates how to use a mimic state: - -.. code-block:: python - :linenos: - - class Car(UP.Actor): - location = UP.CartesianLocationChangingState(recording=True) - speed = UP.State(default=2.0) - riders = UP.State(default_factory=list) - - - class Worker(UP.Actor): - location = UP.State(valid_types=UP.CartesianLocation, recording=True) - car = UP.State() - - - class CarMove(UP.Task): - def task(self, *, actor: Car): - new = UP.CartesianLocation(10, 10) - dist = new - actor.location - time = dist / actor.speed - actor.activate_location_state( - state="location", - speed=actor.speed, - waypoints=[new], - task=self, - ) - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - actor.location - while actor.riders: - rider = actor.riders.pop() - rider.succeed_knowledge_event(name="ARRIVED", cause="here") - - - class WorkerRide(UP.Task): - def task(self, *, actor: Worker): - car: Car = actor.car - actor.activate_mimic_state( - self_state="location", - mimic_state="location", - mimic_actor=car, - task=self, - ) - evt = actor.create_knowledge_event(name="ARRIVED") - # NOT A REHEARSAL-SAFE THING TO DO: - # Better: use a store get/put for real interaction - car.riders.append(actor) - yield evt - print(f"{actor} got event: {evt.get_payload()}: {env.now:.2f}") - actor.deactivate_mimic_state( - self_state="location", - task=self, - ) - - def location_ping(env, time, actors): - while True: - yield env.timeout(time) - for a in actors: - a.location - - with UP.EnvironmentContext() as env: - car = Car(name="Zaphod", location=UP.CartesianLocation(0,0), speed=2) - w1 = Worker(name="Arthur", car=car, location=UP.CartesianLocation(1,1)) - w2 = Worker(name="Trillian", car=car, location=UP.CartesianLocation(1,2)) - - CarMove().run(actor=car) - WorkerRide().run(actor=w1) - WorkerRide().run(actor=w2) - - proc = env.process(location_ping(env, 0.3, [car, w1, w2])) - - env.run(until=8) - print() - print(w1.location) - print(w2.location) - print(car.location) - print() - for i in range(10): - t1, loc1 = w1._location_history[i] - tc, locc = car._location_history[i] - print(((t1 - tc), (loc1.x - locc.x), (loc1.y - locc.y))) - - >>> Worker: Trillian got event: {'cause': 'here'}: 7.07 - >>> Worker: Arthur got event: {'cause': 'here'}: 7.07 - >>> - >>> CartesianLocation(x=10.0, y=10.0, z=0.0) - >>> CartesianLocation(x=10.0, y=10.0, z=0.0) - >>> CartesianLocation(x=10.0, y=10.0, z=0.0) - >>> - >>> (0.0, 1, 1) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - >>> (0.0, 0.0, 0.0) - - -Things to note: - -* Line 26: These riders are put there by another task. This is generally bad form, but works for our small example. - -* Line 34: We are making the worker's CartesianLocation state match the car's CartesianLocationChangingState. - - * Both can be set with/return a CartesianLocation, so this is OK. - - * If we had one State mimicking a LinearChangingState, that would also work since a State can take a floating point value. - - * States type-check under the hood, so you'll get notified if a mimic doesn't match. - -* Line 43: Here we warn again about how this is a bad idea in general. - -* Line 46: Deactivation is always needed. - - * As discussed in :doc:`Rehearsal `, these states are also deactivated on an interrupt. - -No coordination between the actors has to occur for this to work. In this example, the car exiting is done to show the useful nature of mimic states, -and to demonstrate other UPSTAGE features, such as ``create_knowledge_event`` and ``succeed_knowledge_event``. - -As long as the state values are compatible, ``mimic_state`` should make one get its value (when requested) from its mimic. If either is recording, that -will cause the recording to affect both. Finally, if the actor whose state is being mimiced changes or is deleted, then the mimic state may have issues. This -is not explicitly accounted for, and it is up to the user to make sure dependent actors handle those situations. diff --git a/docs/source/user_guide/how_tos/motion_manager.rst b/docs/source/user_guide/how_tos/motion_manager.rst deleted file mode 100644 index 14dd1e9..0000000 --- a/docs/source/user_guide/how_tos/motion_manager.rst +++ /dev/null @@ -1,208 +0,0 @@ -============== -Motion Manager -============== - -The Motion Manager is an UPSTAGE feature that coordinates Actors that are moving and regions of space that may want to be aware when an Actor enters that region (such as a sensor, or region of a forest fire, etc.). - -There are two motion managers. One uses intersection calculations to maintain a discrete-event style of movement, while the other operates at defined time steps. The latter is required to have -motion detection for when the "sensor" and the viewed entities are both moving. - -The built-in ``<>LocationChangingState`` states work with any of the motion managers in the background, by alerting them when those states are made activate. If you want to control which Actors are visible to the -motion manager, there is the :py:class:`~upstage_des.states.DetectabilityState` that can be given to an actor and set to ``False``. - - -Define the Motion Manager -------------------------- - -.. code-block:: python - :linenos: - - from upstage_des.motion.geodetic_model import subdivide_intersection - from upstage_des.geography.intersections import get_intersection_locations - - with UP.EnvironmentContext(): - motion = UP.SensorMotionManager( - intersection_model = subdivide_intersection, - debug=True, - ) - UP.add_stage_variable("motion_manager", motion) - UP.add_stage_variable("intersection_model", get_intersection_locations) - -* Line 1-2: Import one of the intersection models and a support function (more on this below) -* Line 5: Create the :py:class:`~upstage_des.motion.SensorMotionManager` and give it the intersection model - * The other option is the :py:class:`~upstage_des.stepped_motion.SteppedMotionManager` class. (Does not need an intersection) -* Line 9: Add the motion manager to the stage so that the ``<>LocationChangingState`` s can find it. -* Line 10: Add the intersection helper function to the stage so the SensorMotionManager class can find it. - -The ``SensorMotionManager`` does not need to be started or "run", because it only calculates intersection locations and times when something calls its ``_start_mover`` method - which the LocationChangingStates do -in the background. - -Intersection Models -------------------- - -There are two intersection models for ``Geodetic`` locations, and one model for ``Cartesian``. The Stepped motion manager does not require one, it uses :py:func:`~upstage_des.data_types.GeodeticLocation.straight_line_distance` at a given rate. - -* :py:func:`~upstage_des.motion.geodetic_model.subdivide_intersection`: The approximate intersection method with subdivided search, good for WGS84 coordinates. - - * This requires the stage variable ``intersection_model`` to be set. - * The only available intersection model is :py:func:`~upstage_des.geography.intersections.get_intersection_locations` - -* :py:func:`~upstage_des.motion.geodetic_model.analytical_intersection`: An exact intersection using a Spherical earth model. Incompatible with the WGS84 stage model. -* :py:func:`~upstage_des.motion.cartesian_model.cartesian_linear_intersection`: An exact intersection using for XYZ cartesian space. - - -The :py:func:`~upstage_des.geography.intersections.get_intersection_locations` function, required by the subdividing intersection, is what actually finds the intersections. The ``subdivide_intersection`` is -a passthrough function that handles the different earth models, stage variables, and conversion to the format UPSTAGE requires in the ``SensorMotionManager``. The intersection model itself does not have -to know about upstage_des. If you created a ``partial`` of a version of the ``subdivide_intersection`` that took the intersection model as an argument, you would get the same result without needing the stage variable. - - -Sensor Requirements and Example -------------------------------- - -To add a sensor to the motion manager's awareness you must pass it an object that has an attribute for it's location and sensor range. It must also implement ``entity_entered_range`` and -``entity_exited_range`` that accept the entity that is entering/exiting, respectively. - -It is up to the user to decide what to do with that information. They could store it in a queue (such as Store) and process that information later, for example. - -All UPSTAGE does is call one of those methods according to the schedule. - -.. code-block:: python - - from upstage_des.utils import waypoint_time_and_dist - from upstage_des.motion.cartesian_model import cartesian_linear_intersection - - class Bird(UP.Actor): - location = UP.CartesianLocationChangingState() - detectable = UP.DetectabilityState(default=True) - speed = UP.State() - - class Fly(UP.Task): - def task(self, *, actor: Bird): - waypoints = self.get_actor_knowledge(actor, "waypoints") - time, dist = waypoint_time_and_dist(actor.location, waypoints, actor.speed) - actor.activate_location_state( - state="location", - speed = actor.speed, - waypoints = waypoints, - task=self, - ) - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - - class Sensor(UP.Actor): - spot = UP.State(valid_types=(UP.GeodeticLocation, UP.CartesianLocation)) - dist = UP.State(default=100.0, valid_types=float) - - def entity_entered_range(self, entity): - xy = f"({entity.location.x:.2f}, {entity.location.y:.2f})" - print(f"Oh look, A '{entity}' - time: {self.env.now:.2f} - pos: {xy}") - - def entity_exited_range(self, entity): - xy = f"({entity.location.x:.2f}, {entity.location.y:.2f})" - print(f"The {entity} left :( - time: {self.env.now:.2f} - pos: {xy}") - - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager( - intersection_model=cartesian_linear_intersection, - ) - UP.add_stage_variable("motion_manager", motion) - - viewer = Sensor( - name="Birdwatcher", - spot=UP.CartesianLocation(0, 3), - dist=30.0, - ) - motion.add_sensor(viewer, location_attr_name="spot", radius_attr_name="dist") - - eagle = Bird(name="Eagle", location=UP.CartesianLocation(40, 40), speed=3.0) - path = [ - UP.CartesianLocation(1, 4), - UP.CartesianLocation(0, 40), - ] - eagle.set_knowledge("waypoints", path) - Fly().run(actor=eagle) - # Note that we can run without an end time since the sim is very simple - env.run() - >>> Oh look, A 'Bird: Eagle' - time: 8.16 - pos: (22.01, 23.39) - >>> The Bird: Eagle left :( - time: 27.36 - pos: (0.19, 33.00) - - - -Mover Requirements ------------------- - -There are no special requirements for the mover other than they must implement motion by activating a LocationChangingState of some kind. That calls into the motion managers ``_start_mover`` -method that does all the work. - - -Stepped Motion --------------- - -The time-stepping motion manager works by holding a list of sensing entities and detectable entities, and at each time step, it calculates the ``straight_line_distance`` between each pair. - -If the distance is in range, it fires off the ``entity_entered_range`` and marks the entity as in view. If it's out of range and was in view, it calls ``entity_exited_range``. As long as the -location attribute implements ``straight_line_distance``, this manager will work. - -The stepped motion manager might need to start a process to do the time stepping: - -.. code-block:: python - - with UP.EnvironmentContext(): - motion = UP.SteppedMotionManager( - timestep=3/60., - max_empty_events=3, - ) - UP.add_stage_variable("motion_manager", motion) - motion.run() - -In this case, we do need to ``run`` the motion manager. We also give it a timestep to operate at (here in 3 minute steps, if the sim clock runs on "hours"). - -The ``max_empty_events`` is a special parameter to use if you're going to do ``env.run()`` with no ``until``. The stepped motion will run an event every timestep, so your sim will run forever. This -parameter controls how many timesteps with no events queued in the entire sim to consider the simulation to be over and to stop. In general you should always run your sim until a known end point unless -you can be certain it has a guaranteed terminal state. - -The run is optional *only* if the things that will be detected are moving using a LocationChangingState. The stepped manager allows anything with a location attribute to be detectable, and in that -case you need to run the motion manager (and add the entity as a detectable, see below). - -You can try the same bird example with a SteppedMotionManger: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - motion = UP.SteppedMotionManager( - timestep= 3 / 60., - ) - UP.add_stage_variable("motion_manager", motion) - # This part is optional if you're _only_ moving using a LocationChangingState - motion.run() - - viewer = Sensor( - name="Birdwatcher", - spot=UP.CartesianLocation(0, 3), - dist=30.0, - ) - motion.add_sensor(viewer, location_attr_name="spot", radius_attr_name="dist") - - eagle = Bird( - name="Eagle", - location=UP.CartesianLocation(40, 40), - speed=3.0, - ) - path = [ - UP.CartesianLocation(1, 4), - UP.CartesianLocation(0, 40), - ] - eagle.set_knowledge("waypoints", path) - Fly().run(actor=eagle) - # Note that we can run without an end time since the sim is very simple - env.run() - >>> Oh look, A 'Bird: Eagle' - time: 8.20 - pos: (21.92, 23.31) - >>> The Bird: Eagle left :( - time: 27.40 - pos: (0.19, 33.11) - -Notice the slight inaccuracy in the position due to the time stepping. - -.. note:: - - The stepped manager is more flexible to the kinds of things that can be detected. You can use - :py:meth:`~upstage_des.motion.stepped_motion.SteppedMotionManager.add_detectable` to add anything with a - position. diff --git a/docs/source/user_guide/how_tos/multistore_states.rst b/docs/source/user_guide/how_tos/multistore_states.rst deleted file mode 100644 index 39eccd0..0000000 --- a/docs/source/user_guide/how_tos/multistore_states.rst +++ /dev/null @@ -1,46 +0,0 @@ -======================== -Key/Value Resource State -======================== - -The :py:class:`~upstage_des.states.MultiStoreState` is a state, similar to -:py:class:`~upstage_des.states.DictionaryState`, which allows a state to be -a dictionary of ``Store`` or ``Container`` values on string keys. This state -has initialization features to streamline the creation of those objects. - -The use case for this state is for any store or container tracking that has -runtime-definable names beyond the capabilities provided by -:py:class:`~upstage_des.states.ResourceState`. See :doc:`Resource States ` -for more information. This state matches much of the syntax of the ``ResourceState``. - -Here is an example of creating a general ``MultiStoreState``: - -.. code-block:: python - - import upstage_des.api as UP - - class Warehouse(UP.Actor): - storage = UP.MultiStoreState[Store| Container]( - default=Store, - valid_types=(Store, Container), - default_kwargs={"capacity": 100}, - ) - - with UP.EnvironmentContext(): - wh = Warehouse( - name='Depot', - storage = { - "shelf":{"capacity":10}, - "bucket":{"kind": UP.SelfMonitoringContainer, "init": 30}, - "charger":{}, - } - ) - assert wh.storage["shelf"].capacity == 10 - assert wh.storage["bucket"].level == 30 - assert wh.storage["charger"].capacity == 100 - assert wh.storage["charger"].items == [] - -Note how even though the states default to ``Store``, the ``kind`` argument -in the initialization overrides that default. It's only because SimPy stores -and containers have a "capacity" argument that this example doesn't fail. It -is recommended to not mix types if possible to avoid extra work in type checking -or setting appropriate defaults. diff --git a/docs/source/user_guide/how_tos/nucleus.rst b/docs/source/user_guide/how_tos/nucleus.rst deleted file mode 100644 index ba00402..0000000 --- a/docs/source/user_guide/how_tos/nucleus.rst +++ /dev/null @@ -1,149 +0,0 @@ -============ -Task Nucleus -============ - -UPSTAGE provides a more advanced signal-passing interface between tasks and actors with the ``Nucleus``. - -A Nucleus can be attached to an ``Actor``, and ``States`` linked to the Nucleus. When one of those states changes, -an interrupt will be sent to the relevant task networks. - -The basic syntax is this: - -.. code-block:: python - - # Create the nucleus object, attached to an actor - nuc = UP.TaskNetworkNucleus(actor=actor) - # From some task network: - task_net = task_net_factory.make_network() - # Add it to the actor - actor.add_task_network(task_net) - # Tell the nucleus object that this network changes if - # the state names given change - nuc.add_network(task_net, ["state name to watch", "other state"]) - -When any state given to the nucleus changes, nucleus pushes an interrupt to the task network. That interrupt is passed down -as a cause to ``on_interrupt`` as an instance of type :py:class:`~upstage_des.nucleus.NucleusInterrupt`. - -.. code-block:: python - - class SomeTask(UP.Task): - def task(...): - ... - - def on_interrupt(self, *, actor, cause): - if isinstance(cause, UP.NucleusInterrupt): - state_that_changed: str = cause.state_name - state_value: Any = cause.state_value - -From there, you can decide how to handle the interrupt in the usual manner. - -Here is a complete example showing the interaction. The actor has some number that defines the time it takes to -change the number. Once the number changes, the nucleus interrupts the other task network, and the restarting -task increments the results state. - -.. code-block:: python - :linenos: - - class NumberHolder(UP.Actor): - number = UP.State() - results = UP.State(default=0) - - - class NumberChanger(UP.Task): - def task(self, *, actor): - yield UP.Wait(actor.number) - print(f"{self.env.now:.2f}: About to change number") - actor.number += 1 - - - class InterruptedByNucleus(UP.Task): - def task(self, *, actor): - # Yielding an event makes the task halt forever EXCEPT - # when interrupted. - yield UP.Event() - - def on_interrupt(self, *, actor, cause): - print(f"{self.env.now:.2f}: Interrupted with cause: {cause}") - actor.results += 1 - return self.INTERRUPT.RESTART - - - fact = UP.TaskNetworkFactory( - "changer", - {"Runner": NumberChanger}, - {"Runner": {"default": "Runner", "allowed": ["Runner"]}}, - ) - - fact2 = UP.TaskNetworkFactory( - "interrupted", - {"Side": InterruptedByNucleus}, - {"Side": {"default": "Side", "allowed": ["Side"]}}, - ) - - - with UP.EnvironmentContext() as env: - actor = NumberHolder( - name="example", - number=3, - results=0, - ) - nuc = UP.TaskNetworkNucleus(actor=actor) - - task_net = fact.make_network() - actor.add_task_network(task_net) - - task_net_2 = fact2.make_network() - actor.add_task_network(task_net_2) - nuc.add_network(task_net_2, ["number"]) - - actor.start_network_loop("changer", init_task_name="Runner") - actor.start_network_loop("interrupted", init_task_name="Side") - - env.run(until=25) - print(f"Number of nucleus interrupts: {actor.results}") - - >>> 3.00: About to change number - >>> 3.00: Interrupted with cause: NucleusInterrupt: number 4 - >>> 7.00: About to change number - >>> 7.00: Interrupted with cause: NucleusInterrupt: number 5 - >>> 12.00: About to change number - >>> 12.00: Interrupted with cause: NucleusInterrupt: number 6 - >>> 18.00: About to change number - >>> 18.00: Interrupted with cause: NucleusInterrupt: number 7 - >>> Number of nucleus interrupts: 4 - - -To note: - -* Line 51: The nucelus is watching for ``number`` to change the task network that has the ``InterruptedByNucleus`` task. -* Line 57: Results will increment every time the interrupt runs. - - -Nucleus and Rehearsal -===================== - -Nucleus state watchers are not transferred when an Actor is cloned for rehearsal. When a rehearsing actor has a state change, -it does not affect any other networks or the original Actor. Rehearsal only works on a single task network anyway, and so a nucleus -rehearsal wouldn't make sense at this point in UPSTAGE's development. - - -State Sharing with Nucleus -========================== - -A use case for Nucleus is when multiple task networks are sharing a single state and modifying their processing based on that state. This has one key difficulty, which is that -a task cannot interrept itself. If a TaskNetwork changes a state that it is watching, SimPy will fail. It - - -What follows is an example that implements a nucleus allocation. This is not recommended, but is included to demonstrate how far you can stretch Nucleus and upstage_des. Ultimately, -it is just running on SimPy and you can do what you like. Here are some issues/caveats with the following example: - -* None of the tasks are rehearsal-safe (this is OK if you're not going to rehearse) -* Adding nucleus variables/networks within the network that uses them buries the interaction and increases the risk of bugs. - - * It's preferable to define all Nucleus interactions near Actor instantiation for readability - * In the future, it'd be better to have deeper conditions/information in the nucleus. - -* Using a ``DecisionTask`` helps avoid an ``if`` statement in the ``CPUProcess`` task to add the network to the nucleus -* The business logic of the task is overpowered by assistance code, which UPSTAGE tries to avoid as much as possible. - -.. literalinclude:: ../../../../src/upstage_des/test/test_docs_examples/test_nucleus_sharing.py diff --git a/docs/source/user_guide/how_tos/random_numbers.rst b/docs/source/user_guide/how_tos/random_numbers.rst deleted file mode 100644 index 303f36f..0000000 --- a/docs/source/user_guide/how_tos/random_numbers.rst +++ /dev/null @@ -1,40 +0,0 @@ -============== -Random Numbers -============== - -Random numbers are not supplied by UPSTAGE, you are responsible for rolling dice on your own. - -However, UPSTAGE does use them in one area, which is in :py:class:`~upstage_des.events.Wait`, -in the :py:meth:`~upstage_des.events.Wait.from_random_uniform` method. - -The built-in python ``random`` module is used by default, and you can find it on -``stage.random``. It can be instantiated in a few ways: - -.. code-block:: python - - from random import Random - from upstage_des.api import UpstageBase, EnvironmentContext - - base = UpstageBase() - - with EnvironmentContext(random_seed=1234986): - num = base.stage.random.uniform(1, 3) - print(num) - >>> 2.348057489610457 - - rng = Random(1234986) - with EnvironmentContext(random_gen=rng): - num = base.stage.random.uniform(1, 3) - print(num) - >>> 2.348057489610457 - - with EnvironmentContext(): - num = base.stage.random.uniform(1, 3) - print(num) - >>> 2.348057489610457 - -If you want to use your own random number generator, just supply it to the ``random_gen`` -input, or as its own variable with ``UP.add_stage_variable``. - -If you supply it as ``random_gen``, ensure that it has a ``uniform`` method so that the -Wait event can use it. diff --git a/docs/source/user_guide/how_tos/resource_states.rst b/docs/source/user_guide/how_tos/resource_states.rst deleted file mode 100644 index 5270a95..0000000 --- a/docs/source/user_guide/how_tos/resource_states.rst +++ /dev/null @@ -1,113 +0,0 @@ -=============== -Resource States -=============== -.. include:: ../../class_refs.txt - -In the tutorial for the first simulation, there was this example of a resource state: - -.. code-block:: python - - class CheckoutLane(UP.Actor): - customer_queue = UP.ResourceState[SIM.Store]() - - with UP.EnvironmentContext() as env: - lane = CheckoutLane( - name="FirstLane", - customer_queue={ - "kind": UP.SelfMonitoringStore, - "capacity":10, - } - ) - -The obvious question is, why? The following works just fine: - -.. code-block:: python - - import upstage_des.api as UP - import simpy as SIM - - class CheckoutLane(UP.Actor): - customer_queue = UP.State[SIM.Store]() - - - with UP.EnvironmentContext() as env: - queue_store = UP.SelfMonitoringStore(env, capacity=10) - lane = CheckoutLane( - name="FirstLane", - customer_queue=queue_store, - ) - - def proc(): - yield lane.customer_queue.put("thing") - - env.process(proc()) - env.run() - print(lane.customer_queue.items) - >>> ["thing"] - - -There are several reasons for doing this: - -1. To make cloning an Actor for rehearsal more aware and intelligent about stores and containers as states -2. Simplifies actor instantiation. Instead of having to build a store on your own for each actor, the states accept simpler data types and handle the environment for you. -3. Better default behavior instead of needing a partial or a lambda function in the factory. -4. Default expectations, such as being frozen. -5. |State| does not understanding recording of entries or items/counts in stores or containers. - - -In practice, the second and last reasons are the most compelling in our experience. - --------------------------- -Default Resource Arguments --------------------------- - -You can pair setting a default resource with default keyword arguments for its instantiation. This helps -if you want to have default capacities or initial counts. Use the ``default_kwargs`` input to ``ResourceState`` -to accomplish this. Default arguments are overriden by whatever is passed at actor instantiation. - -.. code-block:: python - - class CheckoutLane(UP.Actor): - customer_queue = UP.ResourceState[SIM.Store]() - magazines = UP.ResourceState[SIM.Container]( - default = SIM.Container, - default_kwargs={"capacity": 50, "init": 25}, - ) - - with UP.EnvironmentContext() as env: - lane = CheckoutLane( - name="FirstLane", - customer_queue={ - "kind": UP.SelfMonitoringStore, - "capacity":10, - } - ) - assert lane.magazines.level == 25 - - lane2 = CheckoutLane( - name="SecondLane", - customer_queue={ - "kind": UP.SelfMonitoringStore, - "capacity":10, - }, - magazines = {"init": 10}, - ) - assert lane2.magazines.level == 10 - ------------------------ -Instantiation Arguments ------------------------ - -The input an Actor needs to receive for a ResourceState is a dictionary of: - -* 'kind': The class of the store or container, which is optional if you provided a default. -* 'capacity': A numeric capacity of the store or container. -* 'init': For containers only, an optional starting amount. -* key:value pairs for any other input expected as a keyword argument by the store or container class. - - -If you want to pre-load a store with items, it's recommended to run a process to yield them. That way you get all the -recording you want, if you have it enabled. - -The alternative is to do the trick of ``the_actor.some_store.items.extend(list_of_your_items)``, but that won't get the recording -to work. You could, in a pinch, run ``the_actor.some_store._record("hand-record")``. diff --git a/docs/source/user_guide/how_tos/resources.rst b/docs/source/user_guide/how_tos/resources.rst deleted file mode 100644 index 87d5d9d..0000000 --- a/docs/source/user_guide/how_tos/resources.rst +++ /dev/null @@ -1,83 +0,0 @@ -============== -Resource Types -============== - -UPSTAGE comes with new resource types in addition to the SimPy resources: - -1. :py:class:`~upstage_des.resources.container.ContinuousContainer` -2. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringStore` -3. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringFilterStore` -4. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringContainer` -5. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringContinuousContainer` -6. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringSortedFilterStore` -7. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringReserveContainer` -8. :py:class:`~upstage_des.resources.reserve.ReserveContainer` -9. :py:class:`~upstage_des.resources.sorted.SortedFilterStore` - -The self-monitoring stores are discussed in :doc:`the simulation data section `. -They enable recording of data within the store or container over time, with an optional input for a function to -evaluate when recording on the items in the stores. - -ContinuousContainer -=================== - -This container accepts gets and puts that act continuously, requiring both a rate and time to get or pull at that rate: - -.. code:: python - - tank = UP.ContinuousContainer(env, capacity=10, init=0) - tank.put(rate=3.0, time=2.5) - env.run(until=3.0) - print(tank.level) - >>> 7.5 - -The gets and puts can be done simultaneously, and the container will determine the current level when asked for it. The -container will also, by default, raise errors when it has reach capacity or when it is empty. - -SortedFilterStore -================= - -This store behaves similar to the SimPy ``FilterStore``, except that it also accepts a function that prioritizes the -items in the store. - -.. code:: python - - with UP.EnvironmentContext() as env: - shelf = UP.SortedFilterStore(env) - # pre-load items - shelf.items.extend([(1, "a"), (2, "b"), (1, "b"), (1, "B")]) - - def _proc() -> tuple[float, str]: - ans = yield shelf.get( - filter=lambda x: x[1] == x[1].lower(), - sorter=lambda x: (x[1], -x[0]), - reverse=True, - ) - return ans - - p = env.process(_proc()) - env.run() - print(p.value) - >>> (1, 'b') - -In the above, we filter items to have lower-case letters. Then we sort by ascending alphabetical and -descending numerical. Note the use of ``reverse=True`` and the ``-x[0]`` to do this. That gives us the -tie-breaker between ``(1, "a")`` and ``(1, "b")`` that ignores ``(1, "B")``. - -ReserveContainer -================ - -The reserve container is not a true Container, in that it doesn't hold on queues. It is used to hold first-come -reservations for something numeric. Those requests can be timed out, and then checked on later by the -requestor. This is useful if you want to reserve access to a limited resource, but don't want or need to -hold in a line to do so. - -The public methods on the ``ReserveContainer`` are: - -1. ``reserve(requestor, quantity, expiration=None)``: Hold an amount -2. ``cancel_request(requestor)``: Cancel a hold -3. ``take(requestor)``: Get the amount held - or fail if request expired -4. ``put(amount, capacity_increase=False)``: Put something in the container, optionally increasing capacity. - -The workflow with this resource is to resever, take, then put back when done - if the resource represented isn't -consumable. diff --git a/docs/source/user_guide/how_tos/routines.rst b/docs/source/user_guide/how_tos/routines.rst deleted file mode 100644 index d66dfeb..0000000 --- a/docs/source/user_guide/how_tos/routines.rst +++ /dev/null @@ -1,121 +0,0 @@ -======== -Routines -======== - -The :py:class:`~upstage_des.routines.Routine` class -is designed to provide support for reusable behaviors -that can be yielded from ``Tasks``. They are limited -to only allow UPSTAGE events to be yielded, which allows -them to be rehearsed and support cancelling/interrupt. - -A ``Routine`` should be small and self-contained, such that -any cancellation or interrupt has no side effects to the -task that it is running in. - -To create a ``Routine``, subclass from the provided base class. -You must implement these methods: - -1. ``run()``: The actual event sequence -2. ``cancel()``: Operations to do when the parent task is interrupted -3. ``rehearse()``: Optional specific rehearsal behavior. UPSTAGE - will run your routine otherwise, which will break if your ``run()`` - has an infinite loop. - -The example below is a routine to draw cards until you get a -certain result. - -.. code-block:: python - - import simpy as SIM - import upstage_des.api as UP - from upstage_des.type_help import ROUTINE_GEN - - class CardDrawing(UP.Routine): - def __init__(self, store: SIM.Store, card_value: str) -> None: - # super() is super! - super().__init__() - self.store = store - self.results: list[str] = [] - self.card_value = card_value - - def run(self) -> ROUTINE_GEN: - """Draw cards""" - while True: - evt = UP.Get(self.store) - yield evt - card: str = evt.get_value() - self.results.append(card) - if card == self.card_value: - # Return works, but you can also store - # the answer and access it later. - return self.results - - def cancel(self) -> ROUTINE_GEN: - """Undo the operation.""" - while self.results: - yield UP.Put(self.store, self.results.pop()) - - def rehearsal(self) -> tuple[float, Any | None]: - """Return time and value.""" - # If you return in run(), make sure to return here. - self.results = ["FAKE CARD"] * 3 - return 0.0, self.results - -Then you would use the routine in a task in this way: - -.. code-block:: python - - class PlayCardGame(UP.Task): - def task(self, *, actor: UP.Actor) -> TASK_GEN: - drawn: list[str] = yield CardDrawing( - actor.deck, - "ace", - ) - actor.add_cards_to_hand(drawn) - -You don't have to store the results as an attribute, but if you prefer -to hold onto the routine instance in more complicated circumstances, -this pattern (similar to ``Get().get_value()``) will work: - -.. code-block:: python - - class PlayCardGame(UP.Task): - def task(self, *, actor: UP.Actor) -> TASK_GEN: - drawer = CardDrawing( - actor.deck, - "ace", - ) - yield drawer - actor.add_cards_to_hand(drawer.results) - -This allows you to define and repeat custom actions in your -tasks while reducing the lines of code in your tasks. If the -simulation had several card drawing tasks with different conditions -you could design a ``Routine`` that was usable for all of them. - -Take Care with ``cancel()`` -*************************** - -The ``cancel()`` method of a routine, unlike the interrupt of a ``Task``, -is allowed to send events out. This makes it possible for ``cancel()`` -to have side effects or get hung up on an event longer than a simulation -creator may expect. For example, returning items to a store may hang if -other processes have put items into the store (up to capacity) while the -``Routine`` was running. - -It is up to you to make sure to test if your cancellation is actually zero- -time or not. - -Finally, UPSTAGE will already cancel the event that is being yielded on when -the task is interrupted. Your cancel method doesn't have to worry about that. -Its purpose is to do cleanup and interactions beyond that scope. By the time -the cancel method is called, the yielded event will have already been cancelled. - -Built-In Routines -***************** - -UPSTAGE provides these built-in ``Routines``: - -1. :py:class:`~upstage_des.routines.WindowedGet`: A routine - for getting every item you can from a store in a given time - window. diff --git a/docs/source/user_guide/how_tos/stage_variables.rst b/docs/source/user_guide/how_tos/stage_variables.rst deleted file mode 100644 index fdac283..0000000 --- a/docs/source/user_guide/how_tos/stage_variables.rst +++ /dev/null @@ -1,114 +0,0 @@ -=============== -Stage Variables -=============== - -The ``stage`` is an UPSTAGE feature to allow thread-safe "global" variables accessible by any Actor or Task. - -To add variables to the stage, within the :py:class:`~upstage_des.base.EnvironmentContext` manager use the :py:func:`~upstage_des.base.add_stage_variable` function. - -Once you set a stage variable, it cannot be changed. This is intentional, as the stage is meant to be static. Anything that changes should go through -SimPy or UPSTAGE tasks, states, or processes. - -.. code-block:: python - - class Person(UP.Actor): - - def do_thinking(self): - number = self.stage.my_variable - print(f"'{self}'' is thinking about {number}") - - - class Think(UP.Task): - def task(self, *, actor: Person): - number = self.stage.my_variable - print(f"Think Task is thinking about {number}") - actor.do_thinking() - yield UP.Event() - - - with UP.EnvironmentContext(initial_time=0.0) as env: - UP.add_stage_variable("my_variable", 3.14) - p = Person(name="Arthur") - Think().run(actor=p) - env.run() - - >>> Think Task is thinking about 3.14 - >>> 'Person: Arthur' is thinking about 3.14 - - -Expected Stage Variables -========================= - -Some variables are expected to exist on the stage for some features. These are found in the :py:class:`~upstage_des.base.StageProtocol` protocol, -and are listed below: - -* "altitude_units": A string of "ft", "m", or other distance unit. See :py:func:`~upstage_des.units.convert.unit_convert` for a list. -* "distance_units": A string of distance units -* "stage_model": A model to use for Geodetic calculations. See :doc:`geography` for more. -* "intersection_model": A model to use for motion manager. See :doc:`geography` and :doc:`motion_manager` for more. -* "time_unit": Units of time. See :py:func:`~upstage_des.units.convert.unit_convert` for a list. -* "daily_time_count": For non-standard time values, such as "ticks", this number is used to create logging outputs with "Days". - -If they are not set and you use a feature that needs them, you'll get a warning about not being able to find a stage variable. - -For more information about time units, see :doc:`times`. - - -Accessing Stage through UpstageBase -=================================== - -The :py:class:`~upstage_des.base.UpstageBase` class can be inherited to provide access to ``self.env`` and ``self.stage`` in any object, not just -actors and tasks. The following snippets shows how you might use it for pure SimPy capabilities. - -.. code-block:: python - - class ManagerCode(UP.UpstageBase): - def run(self): - def _proc(): - process_time = self.stage.process_time - yield self.env.timeout(process_time) - - self.env.process(_proc()) - - -Accessing Stage through upstage_des.api -======================================= - -For convenience, you can also do the following: - -.. code-block:: python - - import upstage_des.api as UP - - with UP.EnvironmentContext() as env: - UP.add_stage_variable("altitude_units", "centimeters") - - stage = UP.get_stage() - assert stage.altitude_units == "centimeters" - altitude_units = UP.get_stage_variable("altitude_units") - assert altitude_units == "centimeters" - - -Accessing Stage outside of the EnvironmentContext -================================================= - -There are some times when you may want the Stage to exist outside of the EnvironmentContext. When doing plotting of -geographic entities, for example, having access to the ``stage_model`` is useful. This is also helpful when visualizing -or doing analysis in Jupyter Notebooks, where you don't want to sit inside a context manager. - -For this situation, UPSTAGE provides a way to operate the context manager without needing to be inside the context. - -.. code-block:: python - - import upstage_des.api as UP - from upstage_des.base import create_top_context, clear_top_context - - ctx = create_top_context() - add_stage_variable("example", 1.234) - - assert get_stage_variable("example") == 1.234 - - clear_top_context(ctx) - -The two functions are just wrappers around the context manager's ``__enter__`` and ``__exit__`` methods, but they provide a clearer -idea of what's being done and why. diff --git a/docs/source/user_guide/how_tos/state_sharing.rst b/docs/source/user_guide/how_tos/state_sharing.rst deleted file mode 100644 index b8a111b..0000000 --- a/docs/source/user_guide/how_tos/state_sharing.rst +++ /dev/null @@ -1,68 +0,0 @@ -============= -State Sharing -============= - -State sharing is a way to share a state on an actor between multiple task networks. - -The only currently implemented feature that shares state is the :py:class:`~upstage_des.state_sharing.SharedLinearChangingState`. - -This is an advanced feature that will require a user to subclass and create their own sharing state for their specific use case. - - -Shared Linear Changing State ----------------------------- - -The :py:class:`~upstage_des.state_sharing.SharedLinearChangingState` allows multiple networks to draw from a linear changing state. See the ``test_nucleus_state_share`` test for a complete example. - -In that example, a mothership refuels a flyer, both of which draw from the same ``SharedLinearChangingState`` fuel level. In that example, the flyer actor doesn't directly draw from the mothership. -Instead, the flyer tells the mothership that a draw will happen, and the mothership creates a new task network that draws that fuel from itself. That fuel is in addition to fuel burned while flying. - -The way the state manages this is by (see :doc:`Active States ` for information about active states) holding a list of tasks that are drawing from the state: - -.. code-block:: python - - # grab data from `get_activity_data` - if "task" in data: - rate_to_add: float = data["rate"] - task: Task = data["task"] - if task in rate_tasks: - raise UpstageError( - f"Duplicate task setting a rate {task}" - f"setting {self.name} on {instance}." - "You may have forgotten to deactivate." - ) - rate_tasks[task] = rate_to_add - -and then removing those rates when the state is deactivated from a particular task/task network. - - -Creating Your Own ------------------ - -There's no explicit API for defining your own sharing state, other than to subclass from State or ActiveState and go from there. - -This is because, while UPSTAGE wants to enforce many good practices, it's difficult to enforce a particular workflow from state changes back to the task network. This is why the Nucleus must -be explicitly defined and why it also uses interrupts, rather than another concept, to manage state changes. - -In some cases, you may be able to get away with pure decrements: - -.. code-block:: python - - class Thinker(UP.Actor): - cognition = UP.State[float](valid_types=float, default=1.0) - - class DoThinking(UP.Task): - def task(self, *, actor: Thinker): - task_cognition_needs = self.get_actor_knowledge(actor, "brain power") - if actor.cognition < task_cognition_needs: - raise UP.SimulationError("Not enough brain power!") - actor.cognition -= task_cognition_needs - yield UP.Wait(some_time_for_task) - actor.cognition += task_cognition_needs - -If you do that mechanism, you'll need to handle interrupts that remember how much decrement you applied, then put it back. - -A more difficult use case is a shared state that follows a "allocate full, but someone can use some and you get less" pattern. There's an example of that in :doc:`nucleus`. That concept won't -work directly with an ActiveState because you would need to restart the task to modify time to complete - which would de-allocate and cause some looping problems in the nucleus. - -In general, a shared state is best for when each task just uses or changes part of the state without concern for the other tasks. Nucleus is probably the better way to handle everything else. diff --git a/docs/source/user_guide/how_tos/states.rst b/docs/source/user_guide/how_tos/states.rst deleted file mode 100644 index cc662aa..0000000 --- a/docs/source/user_guide/how_tos/states.rst +++ /dev/null @@ -1,66 +0,0 @@ -====== -States -====== - -States are a core UPSTAGE feature that gives :py:meth:`~upstage_des.actor.Actor` classes their useful and -changeable data. There are advanced state features, such as :doc:`Active States ` and -:doc:`Resource States `. - -The :py:class:`~upstage_des.states.State` class is a python -`descriptor `_. This provides hooks for getting and setting -values stored on the Actor instance under the name of the state while allowing configuration, data recording, -and other features. - -Plain states are created as follows, with nearly all the arguments shown for the state creation: - -.. code-block:: python - - import ustage_des.api as UP - - class Cashier(UP.Actor): - friendliness = UP.State[float]( - valid_types=(float,), - recording=True, - default=1.0, - frozen = False, - record_duplicates = False, - allow_none_default = False, - ) - - -Note the typing of the ``State``, which tells your IDE and ``mypy`` what to expect out. - -State inputs: - -1. ``valid_types``: Types that the state can take for runtime checks. This may be removed in the future. -2. ``recording``: If the state records every time its value changes. -3. ``default``: A default value to use for the state. This allows the actor to be instantiated without it. -4. ``no_init``: If ``True``, keep the state out of the Actor's init, and raise an error if it's input. -5. ``frozen``: If ``True``, any attempt to set the state value throws an exception (default ``False``). -6. ``record_duplicates``: If recording, allow duplicates to be recorded (default ``False``). -7. ``allow_none_default``: If ``True``, the state can have no default value set and not throw the exception. -8. ``default_factory``: Not shown, but provide a function to create the default value. Useful for mutable defaults. - -The ``allow_none_default`` input is useful if you won't have access to the information needed to set a state when -your Actor is instantiated. This is common when you need actors to have mutual references to each other, for example. - -If you set a default and a default factory, UPSTAGE will raise an error to force you to pick one or the other. - -The ``no_init`` input requires a default to be set. The purpose of this setting is to hint to the user that -the state shouldn't be initialized to anything other than the value given by the default settings. This -is useful for states that act like counters and should always start at a default value. If the state -receives an input on initialization, and error will be thrown. - -Some states do not use all these parameters, so consult the specific documentation for more. - -Particular States -################# - -Specific state descriptions and other state features can be found on these pages: - -1. :doc:`Active States ` -2. :doc:`Resource States ` -3. :doc:`Dictionary and Dataclass States ` -4. :doc:`Dictionary Resource State ` -5. :doc:`State Sharing ` -6. :doc:`Mimic States ` diff --git a/docs/source/user_guide/how_tos/task.rst b/docs/source/user_guide/how_tos/task.rst deleted file mode 100644 index 585b184..0000000 --- a/docs/source/user_guide/how_tos/task.rst +++ /dev/null @@ -1,68 +0,0 @@ -===== -Tasks -===== - -The :py:class:`~upstage_des.task.Task` class is one of the fundamental blocks of an UPSTAGE -simulation. It controls the changes to ``Actor`` states and coordinates with the underlying -SimPy event queue. - -Tasks are defined by subclassing from ``Task`` and creating the ``task`` method. In the -example below, the task is properly typed. UPSTAGE provides a type hint for -the generator object that ``task()`` is. Not also that ``actor`` is a required named -argument. - -.. code-block:: python - - import upstage_des.api as UP - from upstage_des.type_help import TASK_GEN - - class Lathe(UP.Actor): - outgoing_bin = UP.ResourceState[UP.SelfMonitoringStore]() - - - class UseLathe(UP.Task): - def task(self, *, actor: Lathe) -> TASK_GEN: - """Run the lathe task.""" - piece = self.get_actor_knowledge("work_piece", must_exist=True) - time = actor.estimate_work_time(piece) - yield UP.Wait(time) - piece.status = "Done" - actor.number_worked += 1 - yield UP.Put(actor.outgoing_bin, piece) - -In that example, the task yields out UPSTAGE :doc:`Events ` only, which is the -typical usage. UPSTAGE will also let you yield a simpy process, but this will raise a warning and is -discouraged as interrupt handling and other services won't work. If a process is yielded on, and your -task is interrupted, the yielded process will receive an interrupt as well. - -Tasks only allow one actor, so use :doc:`Knowledge ` to help -manage interactions or other information. For more complex interactions, see :doc:`State Sharing `. - -Interrupts ----------- - -Task interruption, by default, will raise the usual SimPy exception. The user can add the ``on_interrupt`` method -to their task subclass to handle interruption. That method must accept an actor, a cause object, and must return -a enumerator that tells UPSTAGE how to handle the interruption. - -See :doc:`Interrupts ` for more. - - -Decision Tasks --------------- - -Decision tasks are a special form of Task that does not touch the simulation queue, except for a zero time wait. -The zero-time wait exists so the user knows that other decision tasks may run before the Actor using the task -proceeds on to the next yield statement. - -Subclass and implement ``make_decision`` to use the class. The ``rehearse_decision`` method can also be implemented -to provide rehearsal decision making. That is useful for separating planning and action code when the simpy clock -will not be advancing. - -See :doc:`Decision Tasks ` for more. - -Rehearsal ---------- - -Rehearsal is a feature for estimating the results of a Task on a "cloned" Actor to examine actor state -for planning purposes. See :doc:`Rehearsal ` for more. diff --git a/docs/source/user_guide/how_tos/task_networks.rst b/docs/source/user_guide/how_tos/task_networks.rst deleted file mode 100644 index fcee40f..0000000 --- a/docs/source/user_guide/how_tos/task_networks.rst +++ /dev/null @@ -1,119 +0,0 @@ -============= -Task Networks -============= - -Task Networks have been demonstrated in :doc:`First Simulation `, and this document will add some details to the usage of Task Networks. - -In general, once an Actor has received a Task Network instance, all introspection or modifications of that network goes through the actor. - -Defining a Network -================== - -Define a network by mapping a task name (string) to a task class, then by mapping those task names to the tasks that are allowed to take place after the key task has been performed, with an -optional default. - -.. code-block:: python - - class MoveTask(UP.Task): - # Assume states change and that the next task is defined in the queue - ... - - class ReadTask(UP.Task): - # Assume states change and that the next task is defined in the queue - ... - - task_classes = { - "Move": MoveTask, - "Read": ReadTask, - } - task_links = { - "Move": { - "default": None, - "allowed":["Move", "Read"], - }, - "Read": { - "default": "Move", - "allowed":["Move", "Read"], - }, - } - move_and_read_factory = UP.TaskNetworkFactory("MoveAndRead", task_classes, task_links) - - -The task classes are given names, and those strings are used to define the default and allowable task ordering. The task ordering need to know the default task (can be None) and the allowed tasks. -Allowed tasks must be supplied. If no default is given, an error will be thrown if no task ordering is given when a new task is selected. If the default or the set task queue violates the -allowed rule, an error will be thrown. - -To start a task network on an actor with the factory first make an instance of the network, add it to the actor, then start the loop with or without a queue: - -.. code-block:: python - - net = move_and_read_factory.make_network() - some_actor.add_task_network(net) - some_actor.start_network_loop(net.name, "Move") - -You can either start a loop on a single task, or define an initial queue through the network if desired (but you must somehow define the first task to run): - -.. code-block:: python - - net = move_and_read_factory.make_network() - some_actor.add_task_network(net) - some_actor.set_task_queue(net.name, ["Move", "Move"]) - some_actor.start_network_loop(net.name) - - -Modifying the Task Network --------------------------- - -Task Networks work by running a defined queue of task names, then by selecting ``default`` links until either a new queue is defined or a rule is violated (and you will get an exception). - -You can modify the task network flow using: - -* :py:meth:`upstage_des.actor.Actor.clear_task_queue`: Empty a task queue -* :py:meth:`upstage_des.actor.Actor.set_task_queue`: Add tasks to an empty queue (by string name) - you must empty the queue first. - -These two methods are preferred since they prevent the risks of appending to a queue without looking at the queue. - -Introspecting the Task Network ------------------------------- - -The task network queues can be viewed using: - -* :py:meth:`upstage_des.actor.Actor.get_task_queue`: This requires the network name. -* :py:meth:`upstage_des.actor.Actor.get_all_task_queues`: This will return for all the networks on the actor. - -You can get the names and processes of tasks that are running (and their network names) using: - -* :py:meth:`upstage_des.actor.Actor.get_running_task`: Returns a dataclass with the task name and process object of the task on the defined network. -* :py:meth:`upstage_des.actor.Actor.get_running_tasks`: Returns the same as above, but keyed on task network names. - -You would want the processes to interrupt them, but you can also use :py:meth:`upstage_des.actor.Actor.interrupt_network` to do that. - -Note that the task queue methods won't return the current tasks, just what's defined to run next. Use the running task methods to find the current task. - -A note on TaskNetworkFactory ----------------------------- - -The :py:class:`~upstage_des.task_network.TaskNetworkFactory` class has some convience methods for creating factories from typical use cases: - -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: From a single task, make a network that loops on it. - * Useful for a Singleton task that, for example, receives communications and farms them out or manages other task networks. -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_terminating`: A network that does one task, then freezes for the rest of the simulation. -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_ordered_looping`: A series of tasks with no branching that loops. -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: A series of tasks with no branching that terminates at the end. - -A terminating task network contains a :py:class:`~upstage_des.task.TerminalTask` task at the end, which waits on an un-succeedable event in a rehearsal-safe manner. - - -Running Multiple Networks -========================= - -An actor has no limits to the number of Task Networks it can run. As long as the Actor's states do not overlap in the networks, they can all run in "parallel". Simply keep the network names -unique. - -When adding parallel task networks, you can avoid a name clash with :py:meth:`upstage_des.actor.Actor.suggest_network_name`, and use the resulting name to add the network. When you are done with a network, -it can be deleted from the actor's attributes using: :py:meth:`upstage_des.actor.Actor.delete_task_network`. The task network will still be allowed to run, so make sure it's in a terminal state first. It will -de-clutter the task network introspection methods, though. - -See :doc:`Nucleus ` and :doc:`State Sharing ` for features related to inter-Task Networks "communication". - -If a state is going to be shared, it's best to add it as a nucleus state so that if another task modifies the state, the other networks can be made aware and change. diff --git a/docs/source/user_guide/how_tos/times.rst b/docs/source/user_guide/how_tos/times.rst deleted file mode 100644 index 412de7a..0000000 --- a/docs/source/user_guide/how_tos/times.rst +++ /dev/null @@ -1,42 +0,0 @@ -========== -Time Units -========== - -At the base level, the UPSTAGE clock (the SimPy clock, really) only cares about the number, and does not care -about units. This is not people-friendly, especially for debug logging or inputting times in some cases. - -UPSTAGE has a few features for dealing with units of time. - -The stage variable ``time_unit`` defines what each increment of the clock means. If you don't set it, UPSTAGE assumes -you mean "hours". When you call for ``pretty_now`` from anything inheriting :py:class:`~upstage_des.base.UpstageBase`, -you will get a timestamp string like: "Day 3 - 13:06:21". The actor debug logging uses that method on every log action. - -If you give a time that isn't standard, such as "clock ticks", you'll get back something like "123.000 clock ticks" if the -environment time is 123. For non-standard clock times, you can also define how much time passage constitutes a "day". This -is just a way to track long time passage in a simpler way. If you're simulating logistics on 2D world where there are 125 -"clock ticks" in a day, then setting the stage variable ``daily_time_count`` to 125 will lead to ``pretty_now`` outputs such -as: "Day 1 - 30 clock ticks" for an environment time of 155. - -Wait with units -=============== - -The only feature UPSTAGE currently has that uses the time units for controlling the clock is -the :py:class:`~upstage_des.events.Wait` event. That event takes a ``timeout_unit`` argument that will convert the -given timeout value from the ``timeout_unit`` into the units defined in ``time_unit`` on the stage. If the unit isn't -compatible, then an error will be thrown. - -Allowable time units -==================== - -Time units that UPSTAGE can convert are: seconds, minutes, hours, days, and weeks. The "standard" time values are just -seconds, minutes, and hours. Any other time unit won't use ``pretty_now`` to output "Day - H:M:S" style. Units that -aren't part of those times won't work with the ``Wait`` feature for ``timeout_unit``. - -UPSTAGE tries to lowercase your time units, and allow for some flexibility in saying "s", "second", "hr", "hour", "hours", -and the like. While there are libraries for doing unit conversions, UPSTAGE prefers to have no dependencies other than -SimPy, so it is restricted in that way. - -See the docstring on :py:func:`~upstage_des.units.convert.unit_convert` for more. - -However, all time units are convertible into a single unit on initialization or input data processing, and this is the -recommended way to run your simulations to ensure your units are correct and consistent. diff --git a/docs/source/user_guide/how_tos/typing.rst b/docs/source/user_guide/how_tos/typing.rst deleted file mode 100644 index e0bf1f7..0000000 --- a/docs/source/user_guide/how_tos/typing.rst +++ /dev/null @@ -1,104 +0,0 @@ -================== -Typing and UPSTAGE -================== -.. include:: ../../class_refs.txt - -UPSTAGE runs type checking using ``mypy``, which can be installed with the ``lint`` directive (see CONTRIBUTING.md for more). - -It is recommended to type your simulations to ensure stable running and reduce the chances for bugs. The difficulty with -typing is that often there are circular imports or other issues when making simulations. The following advice will help with -creating good typing for your simulations. - -------------- -Typing States -------------- - -The primary types to care about are the types of a |State|. While the state definition includes a ``valid_types`` entry, that entry -doesn't provide information to static type checkers or to your IDE. |State| objects are ``Generic``, and can have their types defined -in the following way: - -.. code-block:: python - - class Gardener(UP.Actor): - skill_level: UP.State[int](default=0, valid_types=int) - time_in_sun: UP.LinearChangingState[float](default=0.0, recording=True) - -Later, your IDE will know that any ``Gardener`` instance's ``skill_level`` attribute is an integer type. - -.. note:: - - It is a future feature to remove the ``valid_types`` input and just use the type hinting to check. - -These states already have an assigned type, or have a limited scope of types: - -1. :py:class:`~upstage_des.states.DetectabilityState`: This state is a boolean -2. :py:class:`~upstage_des.states.CartesianLocationChangingState`: The output is of type ``CartesianLocation`` -3. :py:class:`~upstage_des.states.GeodeticLocationChangingState`: The output is of type ``GeodeticLocation`` -4. :py:class:`~upstage_des.states.ResourceState`: The type must be a ``simpy.Store`` or ``simpy.Container`` (or a subclass). You can still define the type. -5. :py:class:`~upstage_des.states.CommunicationStore`: This is of type ``simpy.Store`` - ----------------------- -Task and Process Types ----------------------- - -Tasks, Routines, and simpy processes have output types that are ``Generator`` types. -UPSTAGE has a type alias for each of these: - -.. code-block:: python - - from simpy import Environment - from upstage_des.type_help import ROUTINE_GEN, SIMPY_GEN, TASK_GEN - from upstage_des.api import Task, Actor, process, InterruptStates - - class SomeTask(Task): - def task(self, *, actor: Actor) -> TASK_GEN: - ... - - def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: - ... - return self.INTERRUPT.END - - class SimpleRoutine(UP.Routine): - def __init__(self, time: float) -> None: - self.time = time - - def run(self) -> ROUTINE_GEN: - yield UP.Wait(self.time, rehearsal_time_to_complete=self.time * 2) - - @process - def a_simpy_process(env: Environment, wait: float) -> SIMPY_GEN: - yield env.timeout(wait) - -The methods on decision tasks should all return ``None``. - -Routines and SimPy generators expect to be able to receive data into themselves and -to return data, but the ``Task.task()`` methods do not give a return value other than -``None`` to indicate a stopping of the iteration. - --------------------- -Avoiding Circularity --------------------- - -If you have a lot of circularity in your code, such as actors needing to know about each other within their definitions, -there are a few things to try. The first option is to use ``Protocol`` classes to define the interfaces. Then, ensure that your state -definitions, methods, etc. match the protocols. Your protocol will need to inherit from ``Actor`` at some point, otherwise the -type hint system won't let you know about actor specific methods. - -The alternative is to use ``typing.TYPE_CHECKING`` to allow circular imports during type checking. Use a string of the type -instead of the actual type in this case. There are several examples of this in UPSTAGE and in SIMPY, both for circularity and for -making the API easier to understand for type checkers and your IDE. This will allow your IDE to hint more things to you, and can prevent -errors if you use a protocol an forget to add something like ``add_knowledge()`` to it. - -The first thing to check is if inheritence in the actors can solve your problem, but often it cannot. If you actors are in -the same file, you can simply use strings for the types, and you're all set. If they are in separate files, use the above two -ideas. - -.. code-block:: python - - class Manager(UP.Actor): - employees = UP.State[list["Employee"]](default_factory=list) - - - class Employee(UP.Actor): - # Note the string "None", because a None value is treated as no default. - manager = UP.State[Manager | str](default="None") diff --git a/docs/source/user_guide/index.md b/docs/source/user_guide/index.md deleted file mode 100644 index 21f1e4c..0000000 --- a/docs/source/user_guide/index.md +++ /dev/null @@ -1,80 +0,0 @@ -# User Guide - -These pages explain how to use UPSTAGE's primary features with examples. - -Some simple simulations are created that demonstrate the basic features, and then each main UPSTAGE feature is covered in depth. - -First, UPSTAGE lets you create ``Actors`` and modify the ``States`` of those actors through ``Tasks``, which are assembled into ``TaskNetwork``s that allow control flow of the tasks. - -``States`` have multiple features that allow them to be time-varying while respecting discrete event behavior, along with being recordable. More advanced features include "mimicking" other states. - -``Tasks`` and their networks can interacted with in multiple ways, and UPSTAGE provides ``Nucleus`` and Interrupt handling to support that. - -There are also several convenience features for simulation building and running, including entity naming, geographic functions and states, and safe global variables in an environment context manager. - -## Starting Point - -These tutorials cover the core features of Actors, States, Tasks, and Task Networks. The final tutorial is a comparison of SimPy and UPSTAGE which will help motivate why UPSTAGE was created. - -```{toctree} -:caption: Tutorials -:maxdepth: 1 - -tutorials/first_simulation -tutorials/interrupts -tutorials/rehearsal -tutorials/best_practices -tutorials/data -tutorials/simpy_compare -``` - -It is also recommended that you familiarize yourself with how [SimPy runs by itself](https://simpy.readthedocs.io/en/latest/), since -UPSTAGE is a layer on top of that library. - -## Full Examples - -These are complete examples for some of the above tutorials. - -```{toctree} -:caption: Full Examples -:maxdepth: 1 - -tutorials/first_sim_full.rst -tutorials/rehearsal_sim.rst -tutorials/complex_cashier.rst -tutorials/data_creation_example.rst -``` - -## How-to Guides - -These pages detail the specific activities that can be accomplished using UPSTAGE, including how to extend UPSTAGE's features. - -```{toctree} -:caption: How-Tos -:maxdepth: 1 - -how_tos/environment.rst -how_tos/states.rst -how_tos/keyvalue_states.rst -how_tos/multistore_states.rst -how_tos/knowledge.rst -how_tos/task.rst -how_tos/resources.rst -how_tos/resource_states.rst -how_tos/active_states.rst -how_tos/mimic_states.rst -how_tos/nucleus.rst -how_tos/stage_variables.rst -how_tos/times.rst -how_tos/events.rst -how_tos/geography.rst -how_tos/decision_tasks.rst -how_tos/task_networks.rst -how_tos/state_sharing.rst -how_tos/entity_naming.rst -how_tos/motion_manager.rst -how_tos/communications.rst -how_tos/typing.rst -how_tos/random_numbers.rst -how_tos/routines.rst -``` diff --git a/docs/source/user_guide/tutorials/best_practices.rst b/docs/source/user_guide/tutorials/best_practices.rst deleted file mode 100644 index 4e3ca98..0000000 --- a/docs/source/user_guide/tutorials/best_practices.rst +++ /dev/null @@ -1,120 +0,0 @@ -============== -Best Practices -============== - -Actors -====== - -Use knowledge when you want to add built-in enforcement/overwrite checking. -States don't have that by default, so you'd have to write more validation -rules in tasks rather than mostly business logic. - -There's built-in signature and docstring building for Actors based on the -states, but it only works in the interpreter. If you are documenting code, -static docs builders won't pick up on it. - -You can use mixins with Actors, but they cannot have an `__init__` method. -It's better to just subclass an Actor with no states and inherit that with -your other base classes to get modular behavior. - -Tasks -===== - -Keep tasks as small as possible. This makes handling interrupts much easier. -Use the Task Networks to compose smaller tasks, and use decision tasks to navigate the network. - -When doing interrupts, don't be afraid to throw exceptions everywhere. -It's hard to predict what might cause an interrupt (depending), so always -give yourself as much information as you can. - -Mixing nucleus and ``set_knowledge_event`` for task running might get confusing. -Choose nucleus features for task networks that have multiple sources of interrupts. For -simpler holding events (waiting for a job to do, e.g.) that single entity will command -to start, knowledge events are better. - - -Testing -======= - -Write tests for your individual tasks to make sure you see the expected changes. Use -``Task().run(actor=actor)`` in an EnvironmentContext to do that. - -The more clearly defined your stores/interfaces are, the easier it is to test. - -Actor Interactions -================== - -Interaction between different actors is sometimes easier to accomplish with a Task operated -by a higher-level actor that waits for enough actors to say they are ready -(usually via a store). Then the higher-level actor can add knowledge, modify task queues, -and send the actors on their way. - -Even if the behavior being modeled would be decided mutually by the actors (no strict command -hierarchy, e.g.), it can be much easier in DES to run that as a separate process. - -Yielding on a Get is nice for comms and commands, but that usually needs to be a separate task -network with tasks that: - -1. Wait for the message -2. Get the message, decide what to do -3. Analyze the actor's current state -4. Interrupt and recommand as needed - -There are edge cases when you re-command a Task Network, but the re-command/interrupt is sorted -later in the event queue (even with zero-time waits). To mitigate this problem, put very small -(but non-zero) waits after a message is received to give some time for the new task networks to -change, so they are ready for new interrupts if a message immediately follows another. - -Geography -========= - -The ``GeodeticLocationChangingState`` isn't perfectly accurate when it reaches its destination. -Floating point errors and the like will make it be slightly off the destination. - -THe amount of difference will be very small, and practically shouldn't matter in most cases. -Be aware of this, and set locations explicitly after deactivating them if you need the precision. - - -Simulation Determinism -====================== - -While Python 3.10+ generally guarantee that all dictionaries act in an insertion-ordered manner, -that order might change from run to run, even if the random seed is the same. If your simulations -are not deterministic even with a controlled random seed, it is likely due to lack of determinism -in dictionary access or sorting. - -To mitigate this, you'll need to implement some kind of sorting or hashing that is dependent on -something that isn't based on ``id``. - -This issue arises frequently in management logic, where actors are selected from lists or dictionaries -to perform some task. - -Rehearsal -========= - -When testing for ``PLANNING_FACTOR_OBJECT``, do so in a method on the task that streamlines the -business logic of the main task. For example: - -.. code-block:: python - - class DoThing(UP.Task): - @staticmethod - def _get_time(item: UP.PLANNING_FACTOR_OBJECT | dict[str, float]) -> float: - """Return a processing time from an item.""" - if item is PLANNING_FACTOR_OBJECT: - return 3.0 - return item["process_time"] - - def task(self, *, actor: UP.Actor) -> upstage_des.type_help.TASK_GEN: - item = yield UP.Get(actor.some_store, planning_time_to_complete=1.23) - time = self._get_time(item) - yield UP.Wait(time) - -Rehearsals can get very complicated, and tasks that have lots of process interaction expectations -may not rehearse well. Rehearsal is best done for simpler, streamlined tasks. Make sure there -is a clear code path for rehearsing, and following the advice in the Tasks section of this page will go -a long way to making rehearsals better. - -Rehearsal currently only works for one Actor at a time, and while the Actor is clone-able without -affecting the rest of the sim, the ``stage`` is not cloned. If a task references ``stage``, or -looks to other actors, events, stores, etc. the rehearsal may cause side-effects in the actual sim. diff --git a/docs/source/user_guide/tutorials/complex_cashier.rst b/docs/source/user_guide/tutorials/complex_cashier.rst deleted file mode 100644 index b18efda..0000000 --- a/docs/source/user_guide/tutorials/complex_cashier.rst +++ /dev/null @@ -1,8 +0,0 @@ -============================ -Complex Cashier Full Source -============================ - -.. literalinclude:: ../../../../src/upstage_des/test/test_docs_examples/test_cashier_complex.py - - -This file is auto-generated. diff --git a/docs/source/user_guide/tutorials/data.rst b/docs/source/user_guide/tutorials/data.rst deleted file mode 100644 index 0766dfd..0000000 --- a/docs/source/user_guide/tutorials/data.rst +++ /dev/null @@ -1,505 +0,0 @@ -======================================== -Simulation Data Gathering and Processing -======================================== - -UPSTAGE has four features for data recording: - -1. Use ``Actor.log()`` to log a string message at a given time. - - * Note that on Actor creation, ``debug_log=True`` must be given. - -2. Use ``a_state = UP.State(recording=True)``. - - * Access the data with ``actor._state_histories["a_state"]`` - * The data will be in the form ``tuple[time, value]`` - * For :doc:`ActiveStates `, the ``value`` may be - a special ``Enum`` saying if the state is being activated, deactivated, - or is active/inactive. - * Supply additional recording functions with ``recording_functions=[(function, name), ...]`` - -3. Use a ``SelfMonitoring<>`` Store or Container. - - * Access the data with ``a_store._quantities`` - * The data will be in the form ``tuple[time, value]`` - -4. Use the generic data recorder: ``record_data(data_object)``. - - * Access data with ``get_recorded_data()``. - * The data will be in the form ``[(time, data), (time, data), ...]``. - -UPSTAGE also has utility methods for pulling most of the available data into a -tabular format, along with providing column headers. - - -Actor Logging -============= - -The actor debug logging records data about the flow of the actor through its task networks. It's designed -for debugging and seeing how your actors are behaving, but it can be a place to add additional data if -you want to look it up later. - -.. code:: python - - with UP.EnvironmentContext(): - cashier = Cashier( - name="Bob", - debug_log=True, - ) - # Log with an argument logs a message - cashier.log("A message") - # Log without an argument returns the log - print(cashier.log()) - >>> [(0.0, '[Day 0 - 00:00:00] A message')] - -The logging comes a list of tuples. The first entry is the time as a float - useful for filtering. The second -entry is a string of the message along with a formatted time. The time is based on :doc:`Time Units `. - -If you set the stage variable ``debug_log_time`` to ``False`` (it behaves as ``True`` by default), then the actor will -not log the time, and only put the message as the second entry. This message is typed to be a string, but since this -is python, if you aren't running a static type checker on your own code, you can put anything you like there. -If ``debug_log_time`` is ``True``, UPSTAGE will attempt to format it as a string, so make sure it's set to ``False``. - -On a per-actor level, you can set ``debug_log_time`` as well, and that value will take priority over the stage value. - -.. code:: python - - with UP.EnvironmentContext(): - cashier = Cashier( - name="Bob", - debug_log=True, - debug_log_time=False, - ) - cashier.log("A message") - print(cashier.log()) - >>> [(0.0, 'A message')] - - cashier2 = Cashier( - name="Betty", - debug_log=True, - ) - UP.set_stage_variable("debug_log_time", False) - cashier2.log({'data': 1}) - print(cashier2.log()) - >>> [(0.0, {'data': 1})] - - -State Recording -=============== - -Nearly every state is recordable in UPSTAGE. The :py:class:`~upstage_des.states.ResourceState` -is an exception covered in the next section. To enable state recording, set ``recording=True``. -After running the sim, use the ``_state_histories`` attribute on the actor to get the data. - -.. code:: python - - class Cashier(UP.Actor): - items_scanned = UP.State[int](recording=True) - - with UP.EnvironmentContext() as env: - cash = Cashier(name="Ertha", items_scanned=0) - cash.items_scanned += 1 - env.run(until=1) - cash.items_scanned += 2 - env.run(until=2) - cash.items_scanned += 1 - env.run(until=3) - cash.items_scanned = -1 - - print(cash._state_histories["items_scanned"]) - >>> [(0.0, 0), (0.0, 1), (1.0, 3), (2.0, 4), (3.0, -1)] - -That returns a list of (time, value) tuples. This works for simple data types, -but not mutable types: - -.. code:: python - - from collections import Counter - - class Cashier(UP.Actor): - people_seen = UP.State[str](default="", recording=True) - items = UP.State[Counter[str, int]](default_factory=Counter, recording=True) - - with UP.EnvironmentContext() as env: - cash = Cashier(name="Ertha") - cash.people_seen = "James" - cash.items["bread"] = 1 - env.run(until=0.75) - cash.people_seen = "Janet" - cash.items["bread"] += 2 - - print(cash._state_histories) - >>>{'people_seen': [(0.0, 'James'), (0.75, 'Janet')]} - -Note that the string State of ``people_seen`` acts as a way to record data, even if we don't care in -the moment the name of the last scanned person. This lets states behave as carriers of current or past -information, depending on your needs. - -Recording Functions -------------------- - -If a state is recording, it can also record custom data whenever the state updates. This can -provide some capabilities for data tracking inline, without having to post-process. The -state can take either a function or a class object that has a ``__call__`` method that has -a signature that accepts a time and a value of the same type as the state. - -.. note:: - - Recording functions follow the same rule for duplicate recording as the state does. - Same-time recordings only compare to the last entry in the history, so recorded - values can alternate just like the state itself. - - -Future versions of UPSTAGE may update this to allow the actor to be and input. This is not -done currently to avoid accidentally modifying the actor inside the recording. - -.. code:: python - - from collections import Counter - - class NameStorage: - def __init__(self) -> None: - self.seen: dict[str, int] = Counter() - self.seen[""] = 0 - - def __call__(self, time: float, value: str) -> float: - if value: - self.seen[value] += 1 - return max(self.seen.values()) - - def first_letter(time: float, value: str) -> str: - if value: - return value[0] - return "" - - class Cashier(UP.Actor): - people_seen = UP.State[str]( - default="", - recording=True, - recording_functions=[ - (NameStorage, "max_repeats"), - (first_letter, "first_letter"), - ], - ) - - with UP.EnvironmentContext() as env: - cash = Cashier(name="Ertha") - cash.people_seen = "James" - cash.people_seen = "Bob" - cash.people_seen = "James" - cash.people_seen = "Fred" - cash.people_seen = "James" - - print(cash._state_histories["max_repeats"]) - >>> [(0.0, 0), (0.0, 1), (0.0, 2), (0.0, 3)] - - print(cash._state_histories["first_letter"]) - >>> [(0.0, ""), (0.0, "J"), (0.0, "B"), (0.0, "J"), (0.0, "F"), (0.0, "J")] - -.. _complex_states: - -Complex States --------------- - -The ``items`` value doesn't record, because the state doesn't see the ``cash.items = ...`` operation. -For objects like that, you can use the ``record_state`` method on the ``Actor``: - -.. code:: python - - from collections import Counter - - class Cashier(UP.Actor): - items = UP.State[Counter[str, int]](default_factory=Counter, recording=True) - - with UP.EnvironmentContext() as env: - cash = Cashier(name="Ertha") - cash.items["bread"] = 1 - cash.record_state("items") - # or, cash.items = cash.items - env.run(until=0.75) - cash.items["bread"] += 2 - cash.items["milk"] += 3 - cash.record_state("items") - - print(cash._state_histories) - >>>{'items': [(0.0, Counter({'bread': 1})), (0.75, Counter({'bread': 3, 'milk': 3}))]} - -Note also that UPSTAGE deep-copies the value in the state history, so any data should be compatible with that -operation. - -UPSTAGE will output data from ``dataclass`` states, and ``dict[str, Any]`` states by creating rows in the -data table with the naming convention ``state_name.attribute_name``, where the attribute is either a dataclass -attribute or a key from the dictionary. - -.. note:: - - The :doc:`Dictionary State ` was created to mitigate some of these issues. - -Geographic Types ----------------- - -State recording of the built-in geographic states (cartesian and geodetic) is compatible -with the data objects. This for both the active state versions and the typical ``UP.State[CartesianLocation]()`` -ways of creating the state. - -It's recommended, since UPSTAGE does not store much data about the motion of geographic states, to poll or ensure you -get the state value whenever you want to know where it is. While activating and deactivating will record the value, -if an actor is moving along waypoints, each waypoint doesn't record itself unless asked. - -Active State Recording -====================== - -Active states record in the same way, but extra information is given to tell the user if the state -was activated or not and if it was switching to/from active or inactive. - -The state history will still be ``(time, value)`` pairs, but on activation and deactivation an ``Enum`` -value is placed in the history to indicated which has taken place. The state value isn't recorded in -that row of the history because it will have been calculated immediately prior and recorded. - -.. code:: python - - class Cashier(UP.Actor): - time_worked = UP.LinearChangingState(default=0.0, recording=True) - - with UP.EnvironmentContext() as env: - cash = Cashier(name="Ertha") - - cash.activate_linear_state( - state="time_worked", - rate=1.0, - task=None, # this is fine to do outside of a task. - ) - - env.run(until=1) - cash.time_worked - env.run(until=3) - cash.time_worked - cash.deactivate_state(state="time_worked", task=None) - env.run(until=4) - cash.time_worked = 5.0 - - print(cash._state_histories["time_worked"]) - >>> [ - (0.0, 0.0), - (0.0, ), - (1.0, 1.0), - (3.0, 3.0), - (3.0, ), - (4.0, 5.0), - ] - -The built-in data gathering will account for this for you, but if you are manually processing -the active state histories, the (de)activation signal in the history should always come -after a recording at the same time value. - -Remember that if you never ask for the value of ``time_worked``, it will only report it on -activation and deactivation. - -Resource Recording -================== - -If you have a state that is a simpy resource, UPSTAGE won't know how to record that state. For the reasons -discussed above, there's no way to link the changes in the referenced value of the state to the recording -mechanism. Even if there was, there's not an implicit understanding of the nature of the resource. - -UPSTAGE comes with resource types, based on the SimPy types, that automatically record: - -1. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringStore` -2. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringFilterStore` -3. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringContainer` -4. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringContinuousContainer` -5. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringSortedFilterStore` -6. :py:class:`~upstage_des.resources.monitoring.SelfMonitoringReserveContainer` - -Each resource understands the kind of data it can hold, and records it appropriately. Containers are simpler, -and just record the level that they are at. - -The ``SelfMonitoring<>Store`` resources accept an optional ``item_func`` argument, the result of which is put into -the recorded data. By default, the number of items in the store is used. - -The following example shows how to use a monitoring store and get data back from it. The ``_quantities`` attribute -on the state is used to hold the data. - -.. code:: python - - class CheckoutLane(UP.Actor): - belt = UP.ResourceState(default=UP.SelfMonitoringStore) - - with UP.EnvironmentContext() as env: - check = CheckoutLane(name="Lane 1: 10 Items or Fewer") - - # Mix simpy with UPSTAGE for simple processes - def _proc(): - yield check.belt.put("Bread") # simpy event - yield env.timeout(1.0) - yield UP.Put(check.belt, "Milk").as_event() # UPSTAGE event as simpy - yield UP.Put(check.belt, "Pizza").as_event() - - env.process(_proc()) - env.run() - print(check.belt._quantities) - >>> [(0.0, 0), (0.0, 1), (1.0, 2), (1.0, 3)] - -Here's how to set your own item function, omitting the middle portion which stays the same: - -.. code:: python - - from collections import Counter - - class CheckoutLane(UP.Actor): - belt = UP.ResourceState( - default=UP.SelfMonitoringStore, - default_kwargs={"item_func":lambda x: Counter(x)}, - ) - - ... - - print(check.belt._quantities) - >>> [ - (0.0, Counter()), - (0.0, Counter({'Bread': 1})), - (1.0, Counter({'Bread': 1, 'Milk': 1})), - (1.0, Counter({'Bread': 1, 'Milk': 1, 'Pizza': 1})) - ] - -Or use the actor init to pass the item function: - -.. code:: python - - check = CheckoutLane( - name = "Lane 2", - belt = {"item_func":lambda x: Counter(x)}, - ) - - -General Data Recording -====================== - -General data recording is for data that may not conveniently work with states or monitored -stores. UPSTAGE provides a simple interface for storing general information: - -.. code-block:: python - - from upstage_des.data_utils import record_data - - with UP.EnvironmentContext() as env: - ... - record_data("The cashier made a funny joke") - ... - record_data({"received": ["fruit", "eggs"], "shipping method": "car"}) - ... - -The optional parameter ``copy`` can be set to ``True`` to attempt a deep copy of the -object to record a snapshot of a mutable type that may change. - -Data Gathering -============== - -There are three functions for gathering data from UPSTAGE: - -1. :py:func:`upstage_des.data_utils.data_utils.create_table` - - * Finds all actors and their recording states - * Finds all ``SelfMonitoring<>`` resources that are not attached - to actors. - * Ignores location states by default - * Reports actor name, actor type, state name, state value, and - if the state has an active status. - * If ``skip_locations`` is set to ``False``, then location objects - will go into the state value column. - * If ``save_static`` is set to ``True``, then non-recording states - will have their last value recorded in the table with an ``Activation Status`` - column value of ``"Last Seen"``. - * Data are in long-form, meaning rows may share a timestamp. - -2. :py:func:`upstage_des.data_utils.data_utils.create_location_table` - - * Finds all location states on Actors - * Reports location data as individual columns for the dimensions - of the location (XYZ or LLA). - * Reports on active/inactive state data. - * Data are not completely in long-form. XYZ are on a single row, but - rows can have the same timestamp if they are different states. - -3. :py:func:`upstage_des.data_utils.data_recorder.get_recorded_data` - - * Returns the list of tuples of time and data that was recorded. - * No other features, it is up to the user to pick what they want - and how they want to process it. - -Using the example in :doc:`Data Gathering Example `, the -following table (a partial amount shown) would be obtained from the ``create_table`` function: - -.. table:: - - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Entity Name| Entity Type | State Name |Time|Value|Activation Status| - +===========+=========================+=============+====+=====+=================+ - |Ertha |Cashier |items_scanned| 0| 0.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Ertha |Cashier |items_scanned| 3| -1.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Ertha |Cashier |cue | 3| 1.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Ertha |Cashier |cue2 | 3| 11.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Ertha |Cashier |time_working | 3| 2.9|active | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Ertha |Cashier |other | 0| 3.0|Last Seen | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Bertha |Cashier |cue | 0| 0.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Bertha |Cashier |cue2 | 0| 0.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Bertha |Cashier |time_working | 0| 0.0|inactive | - +-----------+-------------------------+-------------+----+-----+-----------------+ - |Store Test |SelfMonitoringFilterStore|Resource | 0| 0.0| | - +-----------+-------------------------+-------------+----+-----+-----------------+ - -The location table will look like the following table. Now how the active states can be -"activating", "active", or "deactivating". Not shown is the "inactive" value, which -is used for when an active state value is changed, but not because it has been set -to change automatically. - -.. table:: - - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Entity Name |Entity Type| State Name |Time| X | Y |Z|Activation Status| - +============+===========+============+====+=======+=======+=+=================+ - |Wobbly Wheel|Cart |location | 0| 1.0000| 1.0000|0|activating | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Wobbly Wheel|Cart |location | 1| 2.5364| 2.2803|0|active | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Wobbly Wheel|Cart |location | 2| 4.0728| 3.5607|0|active | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Wobbly Wheel|Cart |location | 3| 5.6093| 4.8411|0|deactivating | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Wobbly Wheel|Cart |location_two| 0| 1.0000| 1.0000|0|activating | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Wobbly Wheel|Cart |location_two| 1|-0.5051|-0.3170|0|active | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - |Wobbly Wheel|Cart |location_two| 3|-3.5154|-2.9510|0|deactivating | - +------------+-----------+------------+----+-------+-------+-+-----------------+ - -If you were to have ``pandas`` installed, a dataframe could be created with: - -.. code:: python - - import pandas as pd - import upstage_des.api as UP - from upstage_des.data_utils import create_table - - with UP.EnvironmentContext() as env: - ... - env.run() - - table, header = create_table() - df = pd.DataFrame(table, columns=header) - -.. note:: - - The table creation methods must be called within the context, but - the resulting data does not need to stay in the context. - - The exception is that if a state has a value that uses the environment - or the stage, you may see a warning if you try to access attributes or - methods on that object. diff --git a/docs/source/user_guide/tutorials/data_creation_example.rst b/docs/source/user_guide/tutorials/data_creation_example.rst deleted file mode 100644 index d45c514..0000000 --- a/docs/source/user_guide/tutorials/data_creation_example.rst +++ /dev/null @@ -1,8 +0,0 @@ -================================== -Data Gathering Example Full Source -================================== - -.. literalinclude:: ../../../../src/upstage_des/test/test_data_reporting.py - - -This file is auto-generated. diff --git a/docs/source/user_guide/tutorials/first_sim_full.rst b/docs/source/user_guide/tutorials/first_sim_full.rst deleted file mode 100644 index 7a041d5..0000000 --- a/docs/source/user_guide/tutorials/first_sim_full.rst +++ /dev/null @@ -1,8 +0,0 @@ -============================ -First Simulation Full Source -============================ - -.. literalinclude:: ../../../../src/upstage_des/test/test_docs_examples/test_cashier.py - - -This file is auto-generated. diff --git a/docs/source/user_guide/tutorials/first_simulation.rst b/docs/source/user_guide/tutorials/first_simulation.rst deleted file mode 100644 index 260b996..0000000 --- a/docs/source/user_guide/tutorials/first_simulation.rst +++ /dev/null @@ -1,586 +0,0 @@ -======================== -First UPSTAGE Simulation -======================== -.. include:: ../../class_refs.txt - -This simulation will demonstrate the primary features of UPSTAGE in a very -simple scenario. The goal is demonstrate not just the core UPSTAGE features, but the -interaction of UPSTAGE with SimPy. - --------- -Scenario --------- - -A single cashier works at grocery store. They go to the checkout line, -scan groceries, take breaks, and come back to the line. - -The code for the full example can be :doc:`found here `. - -------- -Imports -------- - -We prefer this syntax for importing UPSTAGE and SimPy: - -.. code-block:: python - - import upstage_des.api as UP - import simpy as SIM - - print("hello world") - - --------------------------- -Define an Actor with State --------------------------- - -An UPSTAGE Actor is a container for State, along with methods for modifying the states, -for changing tasks, and recording data. - -Let's imagine our Cashier has the ability to scan items at a certain speed, and some -time until they get a break. We begin by subclassing |Actor| and including two |State| class variables: - -.. code-block:: python - - class Cashier(UP.Actor): - # items per minute - scan_speed = UP.State[float]( - valid_types=(float,), - frozen=True, - ) - # minutes until break - time_until_break = UP.State[float]( - default=120.0, - valid_types=(float,), - frozen=True, - ) - - -Our Cashier is very simple, it contains two states that are primarily data containers -for attributes of the cashier. This is typical for an UPSTAGE Actor. - -The ``scan_speed`` state is defined to require a ``float`` type (UPSTAGE will throw -an error otherwise), and is ``frozen``, meaning that it cannot be changed once defined. -The ``time_until_break`` state is similar, except that a default value of 120 minutes is supplied. - -.. note:: - There is no explicit time dimension in upstage_des. The clock units are up to the user, - and the user must ensure that all times are properly defined. See :doc:`Time Units ` - for more, including using time units in :py:class:`~upstage_des.events.Wait`. - - -Then you will later instantiate a cashier with [#f1]_: - -.. code-block:: python - - cashier = Cashier( - name="Theoden", - scan_speed=10.0, - time_until_break=100.0, - debug_log=True, - ) - -Note that the `name` attribute is required for all UPSTAGE Actors. Also, all inputs are -keyword-argument only for an Actor. The ``debug_log`` input is ``False`` by default, -and when ``True``, you can call ``cashier.log()`` to retrieve an UPSTAGE-generated log -of what the actor has been doing. The same method, when given a string, will record -the message into the log, along with the default logging that UPSTAGE does. - -States are just Python descriptors, so you may access them the same as you would any -instance attribute: ``cashier.scan_speed```, e.g. - -We want to keep track of the number of items scanned, so let's add a state that records -the time at which items are scanned. - - -.. code-block:: python - - class Cashier(UP.Actor): - # items per minute - scan_speed = UP.State[float]( - valid_types=(float,), - frozen=True, - ) - # minutes until break - time_until_break = UP.State[float]( - default=120.0, - valid_types=(float,), - frozen=True, - ) - items_scanned = UP.State[int]( - default=0, - valid_types=(int,), - recording=True, - ) - time_scanning: float = UP.LinearChangingState( - default=0.0, - valid_types=(float,), - ) - - -Note that the keyword-argument ``recording`` has been set to ``True``. Now, -whenever that state is modified, the time and value will be recorded. - - -.. code-block:: python - - with UP.EnvironmentContext() as env: - c = Cashier(name="bob", scan_speed=12.0) - - c.items_scanned += 1 - env.run(until=1.2) - c.items_scanned += 3 - print(c._items_scanned_history) - >>> [(0.0, 1), (1.2, 4)] - - -UPSTAGE creates the recording attribute on the instance with ``_state_histories[]`` -to store the tuples of ``(time, value)`` for the state on all recorded states. This is compatible with -all states, including Locations, Resources, and states that are lists, tuples, or dicts (UPSTAGE makes deep copies). - -For more information on UPSTAGE's data recording, see :doc:`/user_guide/tutorials/data` - -Note that now we have created a SimPy ``Environment`` in ``env`` using the |EnvironmentContext| -context manager. This gives Actor instances access to the simulation clock (``env.now``). The -environment context and features are covered :doc:`here `. - -When we run the environment forward and change the ``items_scanned`` state, the value is -recorded at the current simulation time. - -Let's also make an Actor for the checkout lane, so we have a simple location to store customer queueing: - -.. code-block:: python - - class CheckoutLane(UP.Actor): - customer_queue = UP.ResourceState[SIM.Store]() - - with UP.EnvironmentContext() as env: - lane = CheckoutLane( - name="FirstLane", - customer_queue={ - "kind": UP.SelfMonitoringStore, - "capacity":10, - } - ) - -Here we use the built-in |ResourceState| to use a |SelfMonitoringStore| as an Actor state. The -self-monitoring store is a subclass of the SimPy ``Store`` that records the number of items -in the store whenever there is a get or put. The ``ResourceState`` could accept a default and -not require a definition in the instantiation, but here we are demonstrating how to instantiate -a ``ResourceState`` in a way that lets you parameterize the store's values (in this case, the -kind and the capacity). Other resources, such as containers, will have capacities and initial values. - -Actors also have ``knowledge``, which is a simple dictionary attached to the actor that has an -interface through the actor and tasks. This allows actors to hold runtime-dependent information -that isn't tied to a state. Knowledge can be set and accessed with error-throwing checks for -its existence, or for checks that it doesn't already have a value. An example is given later. - ----------------------------- -Define Tasks for the Cashier ----------------------------- -We want the cashier to do a series of tasks: - -#. Show up to work -#. Go to the checkout lane the "store manager" tells them. -#. Wait for a customer OR break time -#. If customer: Scan items and receive payment -#. If break: take a break, then return to wait. -#. On store closing, leave. - -Let's define the tasks that wait for a customer and check the customer out. - -.. code-block:: python - :linenos: - - from collections.abc import Generator - from upstage_des.type_help import TASK_GEN - - - class WaitInLane(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Wait until break time, or a customer.""" - lane: CheckoutLane = self.get_actor_knowledge( - actor, - "checkout_lane", - must_exist=True, - ) - customer_arrival = UP.Get(lane.customer_queue) - - start_time = self.get_actor_knowledge( - actor, - "start_time", - must_exist=True, - ) - break_start = start_time + actor.time_until_break - wait_until_break = break_start - self.env.now - break_event = UP.Wait(wait_until_event) - - yield UP.Any(customer_arrival, break_event) - - if customer_arrival.is_complete(): - customer: int = customer_arrival.get_value() - self.set_actor_knowledge(actor, "customer", customer, overwrite=True) - else: - customer_arrival.cancel() - self.set_actor_task_queue(actor, ["Break"]) - - - class DoCheckout(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Do the checkout""" - items: int = self.get_actor_knowledge( - actor, - "customer", - must_exist=True, - ) - per_item_time = actor.scan_speed / items - actor.activate_linear_state( - state="time_scanning", - rate=1.0, - task=self, - ) - for _ in range(items): - yield UP.Wait(per_item_time) - actor.items_scanned += 1 - actor.deactivate_all_states(task=self) - # assume 2 minutes to take payment - yield UP.Wait(2.0) - - -Let's step through the task definitions line-by-line. - -* Line 1-2: Typing help. Tasks create generators that yield UPSTAGE Events. - -* Line 4: Create a subclass of a ``Task``. - -* Line 5: Task subclasses must implement ``task`` that takes a single keyword argument: ``actor``. - -* Line 7-11: Assume the cashier has some "knowledge" about the checkout lane - they are going to (the store manager will give this to them). - - * The knowledge has the name "checkout_lane", and we assume it must exist, or else throw an error. - -* Line 12: Create a ``Get`` event that waits to get a customer from the lane's ResourceState. - Note that we aren't yielding on this event yet. - -* Line 14-18: Get information about the actor's break time. - - * We could use ``actor.get_knowledge``, but using the task's method puts extra information - into the actor's log, if you have it enabled. - -* Line 19-21: Get the time left in the sim until it's a break, and create a simple ``Wait`` - event to succeed at that time. - -* Line 23: Yield an ``Any`` event, which succeeds when the first of its sub-events succeeds. - -* Line 25: Test if the customer event succeeded first with the ``Event`` method ``is_complete``. - -* Line 26-27: If it did succeed, call ``get_value`` on the ``Get`` event to get customer information - and add it to our knowledge. - - * Here we just treat the customer information as an integer number of items. It could be anything. - -* Line 29: Cancel the ``Get`` event. Otherwise, it will still exist and take a customer away if one shows up. - - * Later, when discussing interrupting, we'll see how UPSTAGE does this automatically in some instances. - -* Line 30: We haven't covered ``TaskNetworks`` yet, but the ``set_actor_task_queue`` method controls what task happens next. - - * Here we are saying that if we've reached our break time, ignore customers and move on to the ``Break`` task. - - * We didn't define the task to go to if we see a customer, because we'll make that implicit in a few steps. - -* Line 34: Create a task to check the customers out. - -* Line 37-41: Retrieve the knowledge we set in the previous task. - - * Notice how knowledge lets us be flexible about what our Actors can do, and how ``must_exist`` will - help us ensure our tasks are doing the right thing. - -* Line 43-47: Activate a linear changing state, which increases its value according to ``rate`` as the simulation runs. - - * We haven't talked about these yet, but check out the How To's for more: :doc:`/user_guide/how_tos/active_states`. - -* Line 48-50: Scan each item at the specified rate, and increment the cashier's data. - -* Line 51: Stop the ``time_scanning`` linear changing state from accumulating value. - -* Line 53: Assume some follow-on wait for customer payment. - -This is the foundation of how UPSTAGE manages behaviors. The simulation designer creates ``Tasks`` that -can be chained together to perform actions, modify data, and make decisions. - -There is one other kind of Task, a |DecisionTask|, which does not consume the environment clock, -and will not yield any events [#f2]_. - -.. code-block:: python - - class Break(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Decide what kind of break we are taking.""" - actor.breaks_taken += 1 - if actor.breaks_taken == actor.breaks_until_done: - self.set_actor_task_queue(actor, ["NightBreak"]) - elif actor.breaks_taken > actor.breaks_until_done: - raise UP.SimulationError("Too many breaks taken") - else: - self.set_actor_task_queue(actor, ["ShortBreak"]) - - -That task has the ``make_decision`` method that needs to be sublcassed. The purpose of a -`DecisionTask` is to set and clear actor knowledge, and modify the task queue without -consuming the clock. It has additional benefits for rehearsal, which will be covered later. - - -A note on UPSTAGE Events ------------------------- - -UPSTAGE Events are custom wrappers around SimPy events that allow for accessing data about -that event, handling the ``Task`` internal event loop, and for rehearsal. - -All ``Task`` s should yield UPSTAGE events, with one exception. A SimPy ``Process`` can be -yielded out as well, but this will warn the user, and is generally not recommended. - -The event types are: - -#. :py:class:`~upstage_des.events.Event`: Mimics SimPy's raw ``Event``, useful for marking pauses until a success. - - * See :py:meth:`~upstage_des.actor.Actor.create_knowledge_event` for a use case. - -#. :py:class:`~upstage_des.events.All`: Succeed when all passed events succeed - -#. :py:class:`~upstage_des.events.Any`: Succeed when any passed events succeed - -#. :py:class:`~upstage_des.events.Get`: Get from a store or container - -#. :py:class:`~upstage_des.events.FilterGet`: A get with a filter function - -#. :py:class:`~upstage_des.events.Put`: Put something into a store or container - -#. :py:class:`~upstage_des.events.ResourceHold`: Put and release holds on limited resources - -#. :py:class:`~upstage_des.events.Wait`: A standard SimPy timeout - - ------------------------------------- -Define a TaskNetwork for the Cashier ------------------------------------- - -The flow of Tasks is controlled by a TaskNetwork, and the setting of the queue within -tasks. A Task Network is defined by the nodes and the links: - -.. code-block:: python - - task_classes = { - "GoToWork": GoToWork, - "TalkToBoss": TalkToBoss, - "WaitInLane": WaitInLane, - "DoCheckout": DoCheckout, - "Break": Break, - "ShortBreak": ShortBreak, - "NightBreak": NightBreak, - } - - task_links = { - "GoToWork": UP.TaskLinks(default="TalkToBoss",allowed=["TalkToBoss"]), - "TalkToBoss": UP.TaskLinks(default="WaitInLane",allowed=["WaitInLane"]), - "WaitInLane": UP.TaskLinks(default="DoCheckout",allowed=["DoCheckout", "Break"]), - "DoCheckout": UP.TaskLinks(default="WaitInLane",allowed=["WaitInLane"]), - "Break": UP.TaskLinks(default="ShortBreak",allowed=["ShortBreak", "NightBreak"]), - "ShortBreak": UP.TaskLinks(default="WaitInLane",allowed=["WaitInLane"]), - "NightBreak": UP.TaskLinks(default="GoToWork",allowed=["GoToWork"]), - } - - cashier_task_network = UP.TaskNetworkFactory( - name="CashierJob", - task_classes=task_classes, - task_links=task_links, - ) - -The task classes are given names, and those strings are used to define the default and -allowable task ordering. The task ordering need to know the default task (can be None) -and the allowed tasks. Allowed tasks must be supplied. If no default is given, an error -will be thrown if no task ordering is given when a new task is selected. If the default -or the set task queue violates the allowed rule, an error will be thrown. - -The task network forms the backbone of flexible behavior definitions, while a ``DecisionTask`` -helps control the path through the network. - -The ``cashier_task_network`` is a factory that creates network instances from the definition -that actors can use (one per actor/per network). - -To start a task network on an actor with the factory: - -.. code-block:: python - - net = cashier_task_network.make_network() - cashier.add_task_network(net) - cashier.start_network_loop(net.name, "GoToWork") - -You can either start a loop on a single task, or define an initial queue through the network if desired: - -.. code-block:: python - - net = cashier_task_network.make_network() - cashier.add_task_network(net) - cashier.set_task_queue(net.name, ["GoToWork", "TalkToBoss"]) - cashier.start_network_loop(net.name) - - -A note on TaskNetworkFactory ----------------------------- - -The :py:class:`~upstage_des.task_network.TaskNetworkFactory` class has some convience methods -for creating factories from typical use cases: - -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: From a single - task, make a network that loops itself. - - * Useful for a Singleton task that, for example, receives communications and farms them out - or manages other task networks. - -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_terminating`: A network - that does one task, then freezes for the rest of the simulation. - -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_ordered_looping`: A series of - tasks with no branching that loops. - -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: A series of tasks - with no branching that terminates at the end. - --------------------- -Setting up Customers --------------------- - -To complete the simulation, we need to make customers arrive at the checkout lanes. This can -be done using a standard SimPy process: - -.. code-block:: python - - def customer_spawner( - env: SIM.Environment, - lanes: list[CheckoutLane], - ) -> SIMPY_GEN: - # We store the RNG on the stage, and this is a quick way to get the stage (steal it from an actor) - stage = lanes[0].stage - while True: - hrs = env.now / 60 - time_of_day = hrs // 24 - if time_of_day <= 8 or time_of_day >= 15.5: - time_until_open = (24 - time_of_day) + 8 - yield env.timeout(time_until_open) - - lane_pick = stage.random.choice(lanes) - number_pick = stage.random.randint(3, 17) - yield lane_pick.customer_queue.put(number_pick) - yield UP.Wait.from_random_uniform(5.0, 30.0).as_event() - - -Customers arrive every 5 to 30 minutes, and only show up from the hours of 8 AM to 3:30 PM. - ---------------- -Running the Sim ---------------- - -The sim is created with: - -.. code-block:: python - :linenos: - - with UP.EnvironmentContext(initial_time=8 * 60) as env: - UP.add_stage_variable("time_unit", "min") - cashier = Cashier( - name="Bob", - scan_speed=1.0, - time_until_break=120.0, - breaks_until_done=4, - debug_log=True, - ) - lane_1 = CheckoutLane(name="Lane 1") - lane_2 = CheckoutLane(name="Lane 2") - boss = StoreBoss(lanes=[lane_1, lane_2]) - - UP.add_stage_variable("boss", boss) - - net = cashier_task_network.make_network() - cashier.add_task_network(net) - cashier.start_network_loop(net.name, "GoToWork") - - customer_proc = customer_spawner(env, [lane_1, lane_2]) - _ = env.process(customer_proc) - - env.run(until=20 * 60) - - -Going through the lines: - -* Line 1: The simulation starts at 8 AM (in minutes). - -* Line 2: We set a stage variable (accessible through globals) that we are doing time in minutes (just for logging). - -* Line 3-9: Create a cashier that needs breaks every 2 hours, the 4th of which means they can go home. - -* Line 10-12: Create two checkout lanes, and a ``StoreBoss`` that the cashier uses to get a lane assigned. - -* Line 14: Add the ``StoreBoss`` to the global stage. - - * In the ``TalkToBoss`` task, the task calls: ``boss: StoreBoss = self.stage.boss`` - -* Line 16-18: Create and start the task network on the cashier. - -* Lines 20-21: Use SimPy to run the customer event. - -* Line 23: Run for 20 simulation hours. - - -Since only one cashier is assigned, you can examine the backlog on the lanes (and the cashiers progress) with: - -.. code-block:: python - - print(lane_1.customer_queue._quantities) - >>> [(495.0, 0), - >>> (512.0, 1), - >>> (512.0, 0), - >>> (682.913493237309, 1), - >>> (682.913493237309, 0), - >>> (729.4798348277678, 1), - >>> (729.4798348277678, 0), - >>> (783.0901071872663, 1), - >>> (783.0901071872663, 0), - >>> (1087.3217585080076, 1)] - - print(lane_2.customer_queue._quantities) - >>> [(566.5416040656762, 0), - >>> (566.5416040656762, 1), - >>> (622.3573572404293, 2), - >>> (836.9173054961495, 3), - >>> (876.4624776047534, 4), - >>> (926.2323723216172, 5), - >>> (971.9681436809026, 6), - >>> (1033.381298927381, 7), - >>> (1136.5736387094469, 8), - >>> (1188.3694502822516, 9)] - - print(cashier._state_histories["items_scanned"]) - >>> ... - >>> (683.5134932373091, 15), - >>> (683.6134932373092, 16), - >>> (683.7134932373092, 17), - >>> (683.8134932373092, 18), - >>> (683.9134932373092, 19), - >>> (729.6048348277678, 20), - >>> (729.7298348277678, 21), - >>> (729.8548348277678, 22), - >>> (729.9798348277678, 23), - >>> ... - - -Your run may be different, due to the calls to ``stage.random`` (a passthrough for ``random.Random()``). -See :doc:`Random Numbers ` for more. - -Notice how lane 1 takes customers right away, but lane 2 stacks up. Also notice how the -``SelfMonitoringStore`` creates the ``._quantities`` datatype that shows the time history of number of -items in the store. If it was a Container, instead of a Store, it would record the level. - -.. [#f1] You can run this now and ignore the warning about an environment. -.. [#f2] This is not strictly true, it does yield a zero time timeout under the hood. diff --git a/docs/source/user_guide/tutorials/interrupts.rst b/docs/source/user_guide/tutorials/interrupts.rst deleted file mode 100644 index a31a5e1..0000000 --- a/docs/source/user_guide/tutorials/interrupts.rst +++ /dev/null @@ -1,369 +0,0 @@ -=============== -Task Interrupts -=============== - -Task interruption handling is one of UPSTAGE's convenience features -for wrapping SimPy. It allows you to use interruptions to modify the -task network without having to write exceptions over all the ``yield`` -statements. - -Motivation -========== - -For reference, here is how you might handle interruptions in a process with two timeouts: - -.. code-block:: python - - import simpy as SIM - - def env_print(env, message: str) -> None: - msg = f"{env.now:.2f} :: {message}" - print(msg) - - def get_and_work(env: SIM.Environment, store: SIM.Store): - get_event = store.get() - try: - item = yield get_event - env_print(env, f"got the item: '{item}'") - except SIM.Interrupt as interrupt: - # No matter what, cancel the get and leave - get_event.cancel() - env_print(env, f"Interrupted in the get wait. Cause: {interrupt.cause}") - return - - time_to_work = 2.0 # This could be a function of your item - end_time = env.now + time_to_work - while True: - try: - yield env.timeout(time_to_work) - env_print(env, "Finished work") - break - except SIM.Interrupt as interrupt: - env_print(env, f"Interrupted in the work wait with cause: {interrupt.cause}") - if interrupt.cause == "CANCEL GET": - # do nothing, the cancel is too late - # but get ready to loop again! - time_to_work = end_time - env.now - elif interrupt.cause == "CANCEL WORK": - return - - def putter(env, store): - yield env.timeout(1.0) - yield store.put("THING") - - -The work in that process is very simple, but the cancelling and code -required to handle interrupts that we may not care about complicates -it greatly. In general, we prefer a task/process to explain exactly -what it's trying to do, and we can handle interrupts separately without -clouding the business logic of the task. - -In addition, if interrupts are sent by another process, we don't want the -other process to have to know about introspecting the actor or the task -network to know if it can/should do the interrupt. It's preferable to let -the interrupting process ask for an interrupt (with some data) and let -the actor/task decide if it's a good idea or not. Finally, if the simulation -builder does not remember to always cancel events (especially get and put), -then you may end up with a get request that takes from a store without -going anywhere. - -Here's how those interrupts would look in SimPy: - -.. code-block:: python - - # Interrupt in the get wait - env = SIM.Environment() - store = SIM.Store(env) - - proc1 = env.process(putter(env, store)) - proc2 = env.process(get_and_work(env, store)) - - env.run(until=0.5) - proc2.interrupt(cause="outer stop") - env.run() - env_print(env, "DONE") - env_print(env, f"Items in store: {store.items}") - >>> 0.50 :: Interrupted in the get wait. Cause: outer stop - >>> 1.00 :: DONE - >>> 1.00 :: Items in store: ['THING'] - - # Interrupt in the work wait with an ignorable cause - env = SIM.Environment() - store = SIM.Store(env) - - proc1 = env.process(putter(env, store)) - proc2 = env.process(get_and_work(env, store)) - - env.run(until=1.5) - proc2.interrupt(cause="CANCEL GET") - env.run() - env_print(env, "DONE") - >>> 1.00 :: got the item: 'THING' - >>> 1.50 :: Interrupted in the work wait with cause: CANCEL GET - >>> 3.00 :: Finished work - >>> 3.00 :: DONE - - # Interrupt in the work wait with a real cause - env = SIM.Environment() - store = SIM.Store(env) - - proc1 = env.process(putter(env, store)) - proc2 = env.process(get_and_work(env, store)) - - env.run(until=1.5) - proc2.interrupt(cause="CANCEL WORK") - env.run() - env_print(env, "DONE") - >>> 1.00 :: got the item: 'THING' - >>> 1.50 :: Interrupted in the work wait with cause: CANCEL WORK - >>> 3.00 :: DONE - - -If you interrupt without an approved cause, and miss a final ``else`` -(like in this example), you'd finish the work at time 3.5. - -Very critically, if you missed putting the ``get_event.cancel()`` line, -SimPy would still process the ``get_event`` and take the item from the -store. This would effectively remove it from the simulation. - - -UPSTAGE Interrupts -================== - -UPSTAGE's interrupt handling system mitigates these key sources of -error or frustration: - -#. Forgetting to cancel get and put events in an interrupt -#. Handling edge cases when interrupting a get loses an item -#. Cancelling and clearing :doc:`knowledge events `. -#. Make the main task more readable about what it's doing. -#. Simplifies interrupt causes and conditions. -#. Forgetting to stop some calculation of a state - -To access these features, do the following: - -#. Implement ``on_interrupt`` in the :py:class:`~upstage_des.task.Task` class. - -#. Optionally: use the ``marker`` features in the task and interrupt methods. - - * :py:meth:`~upstage_des.task.Task.set_marker` - - * :py:meth:`~upstage_des.task.Task.get_marker` - - * :py:meth:`~upstage_des.task.Task.clear_marker` - -We'll start simple, then add complexity to the interrupt. - -Here's what the above process would look like as an UPSTAGE Task: - -.. code-block:: python - :linenos: - - import upstage_des.api as UP - import simpy as SIM - from typing import Any - - def env_print(env, message: str) -> None: - msg = f"{env.now:.2f} :: {message}" - print(msg) - - - def putter(env, store): - yield env.timeout(1.0) - yield store.put("THING") - - - class DoWork(UP.Task): - def task(self, *, actor: UP.Actor): - self.set_marker("getting") - item = yield UP.Get(actor.stage.store) - env_print(self.env, f"got the item: '{item}'") - - self.set_marker("working") - yield UP.Wait(2.0) - env_print(env, "Finished work") - - def on_interrupt(self, *, actor: UP.Actor, cause: Any) -> UP.InterruptStates: - marker = self.get_marker() - env_print(self.env, f"INTERRUPT:\n\tGot cause: '{cause}'\n\tDuring marker: '{marker}'") - return self.INTERRUPT.END - - -Then, when you run it: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - actor = UP.Actor(name="worker") - store = SIM.Store(env) - UP.add_stage_variable("store", store) - - task = DoWork() - proc = task.run(actor=actor) - - proc1 = env.process(putter(env, store)) - - env.run(until=0.5) - proc.interrupt(cause="because") - env.run() - env_print(env, f"Items in store: {store.items}") - - >>> 0.50 :: INTERRUPT: - >>> Got cause: 'because' - >>> During marker: 'getting' - >>> 1.00 :: Items in store: ['THING'] - -Now the task is small and informative about what it's supposed to do when its -not interrupted. The marker features let us set and get introspection data cleanly. - -Notice also that the ``Get()`` call does not need to be cancelled by the user; -UPSTAGE does that for us (for all :py:class:`~upstage_des.events.BaseEvent` -subclasses that implement ``cancel``). - -Some additional details: - -* Line 25: The ``on_interrupt`` method will pass in the actor and the interrupt cause only. - -* Line 21: If we didn't do: ``self.set_marker("working")`` here, the Task would still - think it was marked as ``"getting"``. Yields do not clear marks. - - * You can use ``clear_marker`` to clear it, and return to a default behavior if you like. - -* Line 26: If no marker is set, the ``get_marker`` method will return ``None`` - -* Line 28: More on ``INTERRUPT`` below. - -If you had created a knowledge event and yielded on it, UPSTAGE will cancel that event, -but also search the Actor's knowledge to see if the event was in the knowledge. If it was, -the event knowledge is cleared. This is done to assis in looping or repeat tasks that -recreate the knowledge event by name for use by other tasks, and this prevents needing to -overwrite the knowledge event or test for its existence in task logic. - -INTERRUPT Types and Setting Markers ------------------------------------ - -Interrupts allow 4 different outcomes to the task, which are signalled by the -:py:class:`~upstage_des.task.InterruptStates` Enum (or -:py:class:`~upstage_des.task.Task.INTERRUPT` as part of ``self``). The first -three can be returned from ``on_interrupt`` to define how to handle the interrupt. - -#. ``END``: Ends the task right there (and moves on in the task network). This cancels the pending event(s). -#. ``IGNORE``: When returned, keeps the task moving along as if the interrupt didn't happen -#. ``RESTART``: Starts the task back over. This cancels the pending event(s). - -UPSTAGE Tasks work by being the process that SimPy sees and managing the internal ``task()`` loop as its own generator, passing SimPy events to the event handler as needed. By default, it assumes -you want to ``END`` the task on an interrupt. This is assumed when no ``on_interrupt`` is defined. - -Markers allow some flexibility in handling interrupts. If you do not define an ``on_interrupt``, then you can use ``self.set_marker(marker, self.INTERRUPT.IGNORE)`` to ignore interrupts while that marker is active. - -If you implement ``on_interrupt``, then the marker's interrupt value is ignored. - - -Advanced Interrupts and Marking -=============================== - -Let's return to our example, and add more complicated interrupt handling, -including with an active state on the actor. - -.. code-block:: python - :linenos: - - class Worker(UP.Actor): - time_worked: float = UP.LinearChangingState(default=0.0) - - class DoWork(UP.Task): - def task(self, *, actor: Worker): - self.set_marker("getting") - actor.activate_linear_state(state="time_worked", rate=1.0, task=self) - item = yield UP.Get(actor.stage.store) - env_print(self.env, f"got the item: '{item}'") - - self.set_marker("working") - yield UP.Wait(2.0) - actor.deactivate_all_states(task=self) - env_print(env, "Finished work") - - def on_interrupt(self, *, actor: Worker, cause: Any) -> UP.InterruptStates: - marker = self.get_marker() - marker_time = self.get_marker_time() - env_print(self.env, f"INTERRUPT:\n\tGot cause: '{cause}'\n\tDuring marker: '{marker}'\nWhich started at: {marker_time}") - - if marker == "getting": - if cause == "CANCEL GET": - time_passed = self.env.now - marker_time - # Allow some leeway that we won't cancel the wait if it's been long enough - if time_passed > 0.9: - return self.INTERRUPT.IGNORE - else: - return self.INTERRUPT.END - else: - return self.INTERRUPT.IGNORE - elif marker == "working": - if cause == "CANCEL WORK": - return self.INTERRUPT.END - else: - return self.INTERRUPT.IGNORE - # A return of None will cause an error, which might be what we want to know. - -The new features are: - -* Line 13: Get the time we set the marker -* Line 18: Use the marker time to determine how we want to interrupt -* Line 31: Remind ourselves that returning ``None`` throws an exception - -With these features we now have separated the logic of a successful task -from one that is interrupted. It also allows more structure and streamlining -of interrupt actions. - -Here's an example where the automatic cancelling of an active state is also shown: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - actor = Worker(name="worker") - store = SIM.Store(env) - UP.add_stage_variable("store", store) - - task = DoWork() - proc = task.run(actor=actor) - - proc1 = env.process(putter(env, store)) - - env.run(until=1.75) - proc.interrupt(cause="CANCEL WORK") - env.run() - env_print(env, f"Time worked: {actor.time_worked}") - - >>> 1.00 :: got the item: 'THING' - >>> 1.75 :: INTERRUPT: - >>> Got cause: 'CANCEL WORK' - >>> During marker: 'working' - >>> Which started at: 1.0 - >>> 3.00 :: Items in store: [] - >>> 3.00 :: Time worked: 1.75 - -The interrupt automatically deactivates all states, keeping your Actors safe from runaway state values. - -Getting the Process -=================== - -If an actor is running a task network, you will need to get the current -Task process to send an interrupt. Do that with the -:py:meth:`upstage_des.actor.Actor.get_running_tasks` method. - -.. code-block:: python - - network_processes = actor.get_running_tasks() - task_name, task_process = network_processes[task_network_name] - task_process.interrupt(cause="Stop running") - - # OR: - task_data = actor.get_running_task(task_network_name) - task_data.process.interrupt(cause="Stop running") - - # OR: - actor.interrupt_network(task_network_name, cause="Stop running") - -The first two methods are better to use if you need to check that the -task name is the right one for interrupt. A well-defined task network -should handle the interrupt anywhere, though. diff --git a/docs/source/user_guide/tutorials/rehearsal.rst b/docs/source/user_guide/tutorials/rehearsal.rst deleted file mode 100644 index 49b9a9a..0000000 --- a/docs/source/user_guide/tutorials/rehearsal.rst +++ /dev/null @@ -1,273 +0,0 @@ -=============== -Task Rehearsal -=============== - -Task Rehearsal is a powerful feature of UPSTAGE, but one that requires some care to implement. The problem that Rehearsal is trying to solve is -to reduce the amount of excess code needed to plan the usage of Actors. - -For example, you may have an Actor that is an Airplane, and you'd like to estimate how long it can fly for. In UPSTAGE, it is possible to rehearse that actor through its -flight path, using planning factors, and see if the final state is feasible. - - -Rehearsing a Single Task -======================== - -Define an actor and a task where some states change: - -.. code-block:: python - - from upstage_des.utils import waypoint_time_and_dist - - class Plane(UP.Actor): - speed = UP.State[float]() - location: UP.CartesianLocation = UP.CartesianLocationChangingState() - fuel: float = UP.LinearChangingState() - fuel_burn = UP.State[float]() - - - class Fly(UP.Task): - def task(self, *, actor: Plane): - fly_to: list[UP.CartesianLocation] = self.get_actor_knowledge(actor, "destination", must_exist=True) - time, dist = waypoint_time_and_dist(actor.location, fly_to, actor.speed) - print(f"Rehearsing the task: {self._rehearsing}") - print(f"\tFlying {dist:.2f} units over {time:.2f} hrs") - actor.activate_linear_state( - state="fuel", - rate=-actor.fuel_burn, - task=self, - ) - actor.activate_location_state( - state="location", - speed=actor.speed, - waypoints=fly_to, - task=self, - ) - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - -Then rehearse a version of it to get a "cloned" actor with new state: - -.. code-block:: python - - with UP.EnvironmentContext() as env: - plane = Plane( - name="Plane", - speed=2.0, - location=UP.CartesianLocation(0, 0), - fuel=120, - fuel_burn=1.5, - ) - - point_1 = UP.CartesianLocation(100, 50) - point_2_options = [ - UP.CartesianLocation(50, 50), - UP.CartesianLocation(100, 75), - ] - - task = Fly() - for point_2 in point_2_options: - fake_plane = task.rehearse( - actor=plane, - knowledge={"destination": [point_1, point_2]}, - ) - print(f"Final fuel: {fake_plane.fuel:.2f}\n") - - print(f"The original plane's fuel: {plane.fuel}") - - >>> Rehearsing the task: True - >>> Flying 161.80 units over 80.90 hrs - >>> Final fuel: -1.35 - >>> - >>> Rehearsing the task: True - >>> Flying 136.80 units over 68.40 hrs - >>> Final fuel: 17.40 - >>> - >>> The original plane's fuel: 120 - - -The key feature is that you call ``rehearse`` on an instance of the task, provide it the actor, and optionally provide any knowledge to give to the actor. Then UPSTAGE runs the task -on a fake environment. - -Limits of Rehearsal -=================== - -Rehearsal currently only works for one Actor at a time, and while the Actor is clone-able without affecting the rest of the sim, the ``stage`` is not cloned. -If a task references ``stage``, or looks to other actors, events, stores, etc. the rehearsal may cause side-effects in the actual sim. - -The actor states and knowledge are shallow copies during rehearsal, which is one part of the risk of side effects. Since UPSTAGE only knows what you tell it to do -through the ``yield``, any effects not going through the ``yield`` will likely cause problems for rehearsal. - -Rehearsing is best for helping planning code determine which actors are capable of doing a series of tasks that have easily separable side-effects. - - -Rehearsing Events, Gets, and Puts -================================= - -When rehearsing UPSTAGE events, we need to tell UPSTAGE how long to run the fake clock for all non-``Wait`` events. We do this by setting ``planning_time_to_complete`` in the event initialization. - -.. code-block:: python - - class ExampleTask(UP.Task): - def task(self, *, actor: UP.Actor): - # Wait for a timeout or an event to succeed - # Pretend 'event' is saved and another process can succeed it - # This is what actor.create_knowledge_event() does (more later) - event = UP.Event(planning_time_to_complete=3.0) - wait = UP.Wait(3.5) - yield UP.Any(event, wait) - # When planning, UP.Any will use the earliest planning time - - -If the planning time for the event were larger than 3.5, then 3.5 would be the time that passes during rehearsal of the ``Any`` event. - -``Get`` events generally provide a value or object from the container or store. For rehearsal purposes, UPSTAGE sends a special object to the task: - -.. code-block:: python - :linenos: - - import simpy as SIM - - class OtherTask(UP.Task): - def task(self, *, actor: UP.Actor): - shelf: SIM.FilterStore = self.stage.a_shelf - # Find an item, which is an object that has a `value` attribute - item = yield UP.FilterGet( - get_location=shelf, - filter=lambda x: x.value >= 10, - rehearsal_time_to_complete=1.0, - ) - time_to_work: float - if item is UP.PLANNING_FACTOR_OBJECT: - time_to_work = 3.0 - else: - time_to_work = item.value / 3.14 - yield UP.Wait(time_to_work) - - class Item: - def __init__(self, value:float): - self.value = value - - class Worker(UP.Actor): - ... - - with UP.EnvironmentContext() as env: - store = SIM.FilterStore(env) - UP.add_stage_variable("a_shelf", store) - - actor = Worker(name="example") - - task = OtherTask() - new_actor = task.rehearse(actor=actor) - print(f"Time of completion: {new_actor.env.now}") - - def proc(): - yield env.timeout(1.0) - yield store.put(Item(value=8)) - yield env.timeout(1.0) - yield store.put(Item(value=314)) - - env.process(proc()) - task.run(actor=actor) - env.run() - print(f"Actual runtime: {env.now}") - - >>> Time of completion: 4.0 - >>> Actual runtime: 102.0 - -Testing if a returned item is a ``PLANNING_FACTOR_OBJECT`` is the only approved way to know if the task is being rehearsed. If there are no -``Get`` events (everything is time-based) - -``Put`` events have a planning time to complete as well, and do not touch the actual stores/containers given to those events. - - -Rehearsing a Task Network -========================= - -You can rehearse paths through a task network as well, to allow more complicated decision making tests. - -In this example, the plane is part of a search and rescue team for natural disaster aid. The plane will fly to as many locations as it -can, perform a search, and then fly somewhere else. At the end, it needs to contingency plan for a landing spot that is as far away as possible. Here -we'll use :doc:`/user_guide/how_tos/decision_tasks` as a way to do task network planning for both running and rehearsing. - -The full example can be found :doc:`here `. - -Here is the planning portion of the TaskNetwork that lets us plan a long route to rehearse on, using ``rehearse_decision`` from ``DecisionTask``. The ``some_preference_function`` is -just a stub for example purposes, showing how to separate the runtime decision logic from the planning logic. - -.. code-block:: python - - class Planner(UP.DecisionTask): - def make_decision(self, *, actor:Plane): - go_to_loc = some_preference_function(self.stage.search_spots) - if go_to_loc is None: # implies we are done with searching - self.set_actor_task_queue(actor, ["Fly", "Land"]) - else: - self.set_actor_knowledge(actor, "destination", go_to_loc, overwrite=True) - self.set_actor_task_queue(actor, ["Fly", "Search"]) - - def rehearse_decision(self, *, actor:Plane): - # Pop off a destination from the queue, or go "home" - next_dests:list[list[UP.CartesianLocation]] | None= self.get_actor_knowledge(actor, "destination_plan", must_exist=False) - dests: list[UP.CartesianLocation] - task_queue: list[str] - if not next_dests: # fly home - dests = [UP.CartesianLocation(0, 0)] - task_queue = ["Fly", "Land"] - else: # pop a location from the plan - dests = next_dests.pop(0) - self.set_actor_knowledge(actor, "destination_plan", next_dests, overwrite=True) - task_queue = ["Fly", "Search"] - - self.set_actor_knowledge(actor, "destination", dests, overwrite=True) - self.set_actor_task_queue(actor, task_queue) - -When we run the rehearsal, we make sure to set ``end_task`` to be ``Land``, so that the network looping takes over from the initial task queue we gave it. If -we hadn't given ``end_task``, the rehearsal would have stopped after the 3 tasks in the ``task_name_list``. - -.. code-block:: python - - with UP.EnvironmentContext() as env: - search_locs = [ - [UP.CartesianLocation(x, y)] - for x, y in [ - (10, 20), - (30, 10), - (15, 15), - ] - ] - - plane = Plane( - name="searcher", - speed=2, - fuel=200, - fuel_burn=5.0, - location=UP.CartesianLocation(20, 10), - debug_log=True, - ) - - net = search_network.make_network() - plane.add_task_network(net) - - new_plane = plane.rehearse_network( - net.name, - task_name_list=["Planner", "Fly", "Search"], - knowledge={"destination_plan": search_locs}, - end_task="Land", - ) - print(f"Fuel left: {new_plane.fuel}") - print(f"Time passed: {new_plane.env.now}") - print(f"Actual time passed: {env.now}") - - >>> Rehearsing the task: True - >>> Flying 14.14 units over 7.07 hrs - >>> Rehearsing the task: True - >>> Flying 0.00 units over 0.00 hrs - >>> Rehearsing the task: True - >>> Flying 22.36 units over 11.18 hrs - >>> Rehearsing the task: True - >>> Flying 15.81 units over 7.91 hrs - >>> Rehearsing the task: True - >>> Flying 21.21 units over 10.61 hrs - >>> Fuel left: 6.181482162082084 - >>> Time passed: 38.76370356758358 - >>> Actual time passed: 0.0 diff --git a/docs/source/user_guide/tutorials/rehearsal_sim.rst b/docs/source/user_guide/tutorials/rehearsal_sim.rst deleted file mode 100644 index dbec0fe..0000000 --- a/docs/source/user_guide/tutorials/rehearsal_sim.rst +++ /dev/null @@ -1,8 +0,0 @@ -============================ -Rehearsal Simulation Example -============================ - -.. literalinclude:: ../../../../src/upstage_des/test/test_docs_examples/test_rehearsing_example.py - - -This file is auto-generated. diff --git a/docs/source/user_guide/tutorials/simpy_compare.rst b/docs/source/user_guide/tutorials/simpy_compare.rst deleted file mode 100644 index d74bbab..0000000 --- a/docs/source/user_guide/tutorials/simpy_compare.rst +++ /dev/null @@ -1,472 +0,0 @@ -================ -UPSTAGE vs SimPy -================ -.. include:: ../../class_refs.txt - -Here we will motivate some of the reason for the existence of UPSTAGE by comparing how things might be done -in both frameworks. SimPy is both simple and powerful, but creating complicated behaviors in agents can result in bulky, -hard to read, and hard to maintain code. Once a simulation designer begins coding large simulations in SimPy, it is our -belief that something like UPSTAGE would emerge to manage the more complexity. - -For this section, we'll use the cashier example from the first tutorial. - -The full final example can be :doc:`found here `. - ----------------- -Basic Simulation ----------------- - -Let's recall the steps of the simulation: - -#. Show up to work -#. Go to the checkout lane the "store manager" tells them. -#. Wait for a customer OR break time -#. If customer: Scan items and receive payment -#. If break: take a break, then return to wait. -#. On store closing, go home, then return in the morning. - -We'll start by importing and making classes for the entities. Then the cashier's process is defined. - -.. code-block:: python - - import simpy as SIM - from dataclasses import dataclass, field - import random - - rnd = random.Random() - - @dataclass - class Cashier: - name: str - scan_speed: float - time_until_break: float = field(default=120.0) - breaks_until_done: int = field(default=2) - breaks_taken: int = field(default=0) - items_scanned: int = field(default=0) - time_scanning: float = field(default=0.0) - - - @dataclass - class CheckoutLane: - name: str - customer_queue: SIM.Store - - - class StoreBoss: - def __init__(self, lanes: list[CheckoutLane]) -> None: - self.lanes = lanes - self._lane_map: dict[str, Cashier] = {} - - def get_lane(self, cashier: Cashier) -> CheckoutLane: - possible = [lane for lane in self.lanes if lane.name not in self._lane_map] - lane = rnd.choice(possible) - self._lane_map[lane.name] = cashier - return lane - - - def checkout_customer(cashier: Cashier, items: int, env: SIM.Environment): - per_item_time = cashier.scan_speed / items - for _ in range(items): - yield env.timeout(per_item_time) - cashier.items_scanned += 1 - # taking payment - yield env.timeout(2.0) - - - def run_cashier(cashier: Cashier, store_boss: StoreBoss, env: SIM.Environment) -> None: - while True: - # go to work - yield env.timeout(15.0) - - # talk to the boss - lane: SIM.Store = store_boss.get_lane(cashier) - - # reset for the day - cashier.breaks_taken = 0 - - # start the waiting in lane loop - while True: - break_start = env.now + cashier.time_until_break - wait_until_break = break_start - env.now - need_break = wait_until_break <= 0 - if not need_break: - break_wait = env.timeout(wait_until_break) - cust_get = lane.customer_queue.get() - ret = yield break_wait | cust_get - if break_wait.processed: - need_break = True - if cust_get in ret: - yield lane.customer_queue.put(ret[cust_get]) - else: - cust_get.cancel() - else: - yield from checkout_customer(cashier, ret[cust_get], env) - - if need_break: - # take a break - cashier.breaks_taken += 1 - if cashier.breaks_taken == cashier.breaks_until_done: - # go to a night rest - break - else: - yield env.timeout(15.0) - - # night break - yield env.timeout(60 * 12.0) - - - def customer_spawner( - env: SIM.Environment, - lanes: list[CheckoutLane], - ): - while True: - hrs = env.now / 60 - time_of_day = hrs // 24 - if time_of_day <= 8 or time_of_day >= 15.5: - time_until_open = (24 - time_of_day) + 8 - yield env.timeout(time_until_open) - - lane_pick = rnd.choice(lanes) - number_pick = rnd.randint(3, 17) - yield lane_pick.customer_queue.put(number_pick) - t = rnd.random() * 25.0 + 5 - yield env.timeout(t) - -Then we can run the sim with: - -.. code-block:: python - - env = SIM.Environment() - cashier = Cashier( - name="Bob", - scan_speed=1.0, - time_until_break=120.0, - breaks_until_done=4, - ) - lane_1 = CheckoutLane(name="Lane 1", customer_queue=SIM.Store(env)) - lane_2 = CheckoutLane(name="Lane 2", customer_queue=SIM.Store(env)) - boss = StoreBoss(lanes=[lane_1, lane_2]) - - customer_proc = env.process(customer_spawner(env, [lane_1, lane_2])) - cashier_proc = env.process(run_cashier(cashier, boss, env)) - env.run(until=20 * 60) - - cashier.items_scanned - >>> 83 - - ------------------ -First Impressions ------------------ - -Much of the simulation feels the same, and it feels somewhat more streamlined (it is half as many lines of code). - -However, the sim is not currently instrumented to record any data, so we don't know when items are scanned, when the customer queue is how full when, etc. We could add that with: - -.. code-block:: python - - @dataclass - class Cashier: - ... - items_scanned_data: list[tuple[float, int]] - ... - - ... - - def checkout_customer(cashier: Cashier, items: int, env: SIM.Environment): - per_item_time = cashier.scan_speed / items - for _ in range(items): - yield env.timeout(per_item_time) - prev = cashier.items_scanned_data[-1][1] - cashier.items_scanned_data.append((env.now, prev + 1)) - # taking payment - yield env.timeout(2.0) - - ... - -This gets bulky in any reasonably sized simulation. If you want to turn on or off the recording of certain values, that would have to be controlled within the business logic, adding to -code bloat and hurting readability. - -If you want to record something new, you have to find all the points where it changes and add in the data recording code. Additionally, there is no built-in way to record information -about store events. - -Finally, the second `while` loop has a bulky control flow that will break down if more complicated actions are needed. The sim is simple for now because the cashier only follows a simple -linear story (with one IF/OR for the break). - -The other factor keeping the code simple is that no methods or entities are interacting. - ---------------------------- -Interrupts and Complex Flow ---------------------------- - -Once your research into cashier logistics is going well with the existing code base, the grocery store chain (who is paying for the research) asks what happens to items scanned per minute -if the store manager needs to ask a cashier to go restock a shelf. Now we need to have the cashier know about a request, finish the checkout, then go do the restock. This should also -respect the break time rules. - -We would start by doing this (assume any unseen methods are appropriate): - -.. code-block:: python - - class Cashier: - ... - requests: SIM.Store - - ... - # inside run_cashier - break_wait = env.timeout(wait_until_break) - cust_get = lane.customer_queue.get() - restock = cashier.requests.get() - ret = yield break_wait | cust_get | restock - # figure out which one happened, then do that task - if break_wait.processed: - if restock.processed: - # tell the manager we can't restock yet - yield store_boss.tell(cashier, "I'll do it later") - # TODO: How would we take a break THEN go restock? - else: - # break-taking code - ... - elif restock.processed: - # some code to restock - elif cust_get.processed: - # process the customer - ... - -The code paths become more complicated the more things the cashier can do. Especially if you want to handle the case where a manager asks for a restock when the cashier is about to go -on break. And the more behaviors and jobs that the simulation needs the cashier or employee to do, the more complicated this code section becomes. - ---------------- -UPSTAGE Version ---------------- - -In UPSTAGE, we can use the |Task| and |TaskNetworks| to manage the behaviors in a more forward-compatible manner that preserves the readability of the business logic. - -Now that we know we need to simulate tasking from the manager that alters our process flow, let's create a new task for that purpose. Here we are building off of :doc:`the original sim `. - -Also note that we aren't setting up the comms receive task yet. That's later. - -.. code-block:: python - - class Cashier(UP.Actor): - ... - # Messages store for when we do comms - messages = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, - ) - - def time_left_to_break(self): - elapsed = self.env.now - self.get_knowledge("start_time", must_exist=True) - return self.time_until_break - elapsed - - - class InterruptibleTask(UP.Task): - def on_interrupt(self, *, actor: Cashier, cause: Any) -> InterruptStates: - # We will only interrupt with a dictionary of data - assert isinstance(cause, dict) - job_list: list[str] - - if cause["reason"] == "BREAK TIME": - job_list = ["Break"] - elif cause["reason"] == "NEW JOB": - job_list = cause["job_list"] - else: - raise UP.SimulationError("Unexpected interrupt cause") - - # determine time until break - time_left = actor.time_left_to_break() - # if there are only five minutes left, take the break and queue the task. - if time_left <= 5.0 and "Break" not in job_list: - job_list = ["Break"] + job_list - - # Ignore the interrupt, unless we've marked it to know otherwise - marker = self.get_marker() or "none" - if marker == "on break": - if "Break" in job_list: - job_list.remove("Break") - - self.clear_actor_task_queue(actor) - self.set_actor_task_queue(actor, job_list) - if marker == "cancellable": - return self.INTERRUPT.END - return self.INTERRUPT.IGNORE - -That |Task| class can be inherited to expect interrupts, modify the task network, and move forward. - -With that framework, we can modify the customer wait task: - -.. code-block:: python - - class WaitInLane(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Wait until break time, or a customer.""" - lane: CheckoutLane = self.get_actor_knowledge( - actor, - "checkout_lane", - must_exist=True, - ) - customer_arrival = UP.Get(lane.customer_queue) - - self.set_marker(marker="cancellable") - yield customer_arrival - - customer: int = customer_arrival.get_value() - self.set_actor_knowledge(actor, "customer", customer, overwrite=True) - - -Notice that it is much simpler since we aren't doing the `Any` wait now. The marker is set to tell the interruption to -end the task, rather than the default of attempting to finish it. - -We make the `InterruptibleTask` as the base class for all the non-decision tasks. - -We also modify the short break task to allow for interrupts. We could use the `InterruptibleTask` - -.. code-block:: python - - class ShortBreak(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Take a short break.""" - self.set_marker("on break") - yield UP.Wait(15.0) - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - # The break timing will go here - ------------------ -Break Timing Task ------------------ - -The break timing and interrupting can be handled separately by this task: - -.. code-block:: python - - class CashierBreakTimer(UP.Task): - def task(self, *, actor: Cashier): - yield UP.Wait(actor.time_until_break) - actor.interrupt_network("CashierJob", cause=dict(reason="BREAK TIME")) - - -And the break timer is run inside the `TalkToBoss` and `ShortBreak` tasks when the start time is set. This is somewhat poor practice, -but for the sake of keeping the example relatively simple, we'll take this direct route. - -.. code-block:: python - - class TalkToBoss(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Zero-time task to get information.""" - ... - self.set_actor_knowledge(actor, "start_time", self.env.now) - # Convenient spot to run the timer. - CashierBreakTimer().run(actor=actor) - - ... - - class ShortBreak(UP.Task): - def task(self, *, actor: Cashier): - """Take a short break.""" - yield UP.Wait(15.0) - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - CashierBreakTimer().run(actor=actor) - - -Running the example as is works the same as before, but now the task flow is interrupted safely by a timing task, removing code within -each task that was checking for that time. This increases the overall amount of code, but reduces the per-task code, leading to -more readable and direct reasoning about the tasks. This also builds the foundation for extensiblity of behaviors and tasks, because -we have mostly separated the logic of task flow, and task behavior. - -We must also change the decision task for breaks to account for us now adding to the task queue after break. - -.. code-block:: python - - class Break(UP.DecisionTask): - def make_decision(self, *, actor: Cashier): - """Decide what kind of break we are taking.""" - actor.breaks_taken += 1 - - # we might have jobs queued - queue = self.get_actor_task_queue(actor) or [] - if "Break" in queue: - raise UP.SimulationError("Odd task network state") - self.clear_actor_task_queue(actor) - - if actor.breaks_taken == actor.breaks_until_done: - self.set_actor_task_queue(actor, ["NightBreak"]) - elif actor.breaks_taken > actor.breaks_until_done: - raise UP.SimulationError("Too many breaks taken") - else: - self.set_actor_task_queue(actor, ["ShortBreak"] + queue) - - ---------------- -Adding Messages ---------------- - -Message handling is added with this code: - -.. code-block:: python - - class CashierMessages(UP.Task): - def task(self, *, actor: Cashier): - getter = UP.Get(actor.messages) - yield getter - tasks_needed: list[str] | str = getter.get_value() - tasks_needed = [tasks_needed] if isinstance(tasks_needed, str) else tasks_needed - actor.interrupt_network( - "CashierJob", - cause=dict(reason="NEW JOB", job_list=tasks_needed), - ) - - cashier_message_net = UP.TaskNetworkFactory.from_single_looping("Messages", CashierMessages) - - ... - - # def run_cashier()... - ... - net = cashier_task_network.make_network() - cashier.add_task_network(net) - cashier.start_network_loop(net.name, "GoToWork") - cnet = cashier_message_net.make_network() - cashier.add_task_network(cnet) - cashier.start_network_loop(cnet.name, "CashierMessages") - - -Here the task will loop forever, coming back to wait for a message and pass along the interrupt after formatting the message data. - -Now it's up to the simulation creator to add a task for restock that goes back to the checkout lane when it's done. Hint: use the default paths in the task network to do that! The -cashier remembers the assigned checkout lane in its knowledge. The only other thing to do is create a task (or a simpy process if we are being simple) to get the store manager to farm -out tasks: - -.. code-block:: python - - class manager_process(boss: StoreBoss, cashiers: list[Cashier]): - while True: - # Use the random uniform feature, but convert the UPSTAGE event to simpy - # because this is a simpy only process - yield UP.Wait.from_random_uniform(15., 30.).as_event() - cash = rnd.choice(cashiers) - yield cash.messages.put(["Restock"]) - - ... - env.process(manager_process(boss, [cashier])) - ... - -------------------- -UPSTAGE Perspective -------------------- - -What this example has hopefully shown is that UPSTAGE provides (besides the benefit of built-in data recording for all objects) -a foundation for future-friendly simulations. - -There is a cost to that friendliness, which is the verbosity of some of the overhead code in creating tasks, the networks, etc. -It is our belief that this verbosity is a benefit rather than a true cost. It is very easy to make task flow mistakes if there -aren't lots of rules and checks, especially as simulations find emergent results and values of states that a simulation designer -may not have anticipated. Similarly, simulations are generally never finished, as new features and behaviors are often added. -UPSTAGE provides a way to add those behaviors with minimal effort or changes to the existing code. - -There is also no one way to make an UPSTAGE simulation. An alternative way to handle break timing and messages would have been -through setting knowledge. A decision task could be run after the `ShortBreak` to choose what to do next, or have been run as part -of a loop where every cashier action ends with a decision task that looks at the requested jobs and plans. Either way would work, -and both would take advantage of UPSTAGE's features for task, event, and state cleanup and tracking. - -The key perspective of UPSTAGE is to put in the effort up front to make modular, atomic tasks that handle interrupts -gracefully. Then use task networks to control their flow. Use "parallel" message passing and handling tasks or task networks -to interrupt the primary task networks safely. By separating what an actor can do (and how it does it) from deciding what it should -do, our simulations can grow larger without sacrificing stability or readability. diff --git a/jupyterlite/.nojekyll b/jupyterlite/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/jupyterlite/content/RunCashier.ipynb b/jupyterlite/content/RunCashier.ipynb deleted file mode 100644 index a387679..0000000 --- a/jupyterlite/content/RunCashier.ipynb +++ /dev/null @@ -1,243 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "42e233cb", - "metadata": {}, - "outputs": [], - "source": [ - "# Before doing anything, we must runtime install some libraries into jupyterlite\n", - "%pip install -q nbformat plotly upstage-des>=0.4,<5" - ] - }, - { - "cell_type": "markdown", - "id": "1869cbe9", - "metadata": {}, - "source": [ - "# Cashier Simulation\n", - "\n", - "This is an example model built using UPSTAGE, where cashiers check customers through checkout lanes, help restock shelves, and have periodic breaks." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ec8f6a2", - "metadata": {}, - "outputs": [], - "source": [ - "import upstage_des.api as UP\n", - "from upstage_des.data_utils import create_table\n", - "from model.cashier_model import (\n", - " Cashier,\n", - " CheckoutLane,\n", - " cashier_task_network,\n", - " cashier_message_net,\n", - " customer_spawner,\n", - " manager_process,\n", - " StoreBoss,\n", - ")\n", - "from model.helpers import to_step, doing_to_gantt\n", - "import pandas as pd\n", - "from datetime import datetime, timedelta\n", - "\n", - "import plotly.express as px" - ] - }, - { - "cell_type": "markdown", - "id": "ebfb4150", - "metadata": {}, - "source": [ - "## Sim Creation\n", - "\n", - "The simulation is created inside the `EnvironmentContext`. Note that we are controlling the random seed, setting a start time other than 0, and that data gathering must happen in the context.\n", - "\n", - "Normally you'd want to put most of this code inside a function:\n", - "\n", - "```python\n", - "def build_sim(sim_data) -> SimObjects:\n", - " ...\n", - "\n", - "with UP.EnvironmentContext() as env:\n", - " simobj = build_sim(some_data_source)\n", - " env.run()\n", - "```\n", - "\n", - "but for now we'll put everything in one place so you can see the machinery." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1b777a07", - "metadata": {}, - "outputs": [], - "source": [ - "with UP.EnvironmentContext(initial_time=8 * 60, random_seed=2881680) as env:\n", - " UP.add_stage_variable(\"time_unit\", \"min\")\n", - " cashier = Cashier(\n", - " name=\"Bob\",\n", - " scan_speed=1.0,\n", - " time_until_break=160.0,\n", - " breaks_until_done=4,\n", - " debug_log=True,\n", - " )\n", - " cashier2 = Cashier(\n", - " name=\"Ertha\",\n", - " scan_speed=1.0,\n", - " time_until_break=160.0,\n", - " breaks_until_done=4,\n", - " debug_log=True,\n", - " )\n", - " lane_1 = CheckoutLane(name=\"Lane 1\")\n", - " lane_2 = CheckoutLane(name=\"Lane 2\")\n", - " boss = StoreBoss(lanes=[lane_1, lane_2])\n", - "\n", - " UP.add_stage_variable(\"boss\", boss)\n", - "\n", - " for cash in [cashier, cashier2]:\n", - " net = cashier_task_network.make_network()\n", - " cash.add_task_network(net)\n", - " cash.start_network_loop(net.name, \"GoToWork\")\n", - "\n", - " net = cashier_message_net.make_network()\n", - " cash.add_task_network(net)\n", - " cash.start_network_loop(net.name, \"CashierMessages\")\n", - "\n", - " customer_proc = customer_spawner(env, [lane_1, lane_2], max_wait=6.)\n", - " _ = env.process(customer_proc)\n", - "\n", - " _ = env.process(manager_process(boss, [cashier, cashier2]))\n", - "\n", - " # Optional forcing of a restock before a break to test behavior\n", - " # def _proc():\n", - " # yield env.timeout(8*60 + 10)\n", - " # yield cashier2.messages.put([\"Restock\"])\n", - " # env.process(_proc())\n", - "\n", - " env.run(until=20 * 60)\n", - " data, cols = create_table()" - ] - }, - { - "cell_type": "markdown", - "id": "6e31ffa7", - "metadata": {}, - "source": [ - "## Data Processing\n", - "\n", - "Create a pandas dataframe of the data and convert the time to a datetime for use with Plotly." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "99ac332a", - "metadata": {}, - "outputs": [], - "source": [ - "df = pd.DataFrame(data, columns=cols)\n", - "# convert time to a datetime\n", - "start = datetime(2025, 6, 10)\n", - "df[\"Time\"] = df.Time.apply(lambda x: start + timedelta(minutes=x))" - ] - }, - { - "cell_type": "markdown", - "id": "f5d05de1", - "metadata": {}, - "source": [ - "## Visualization\n", - "\n", - "The data from UPSTAGE is not immediately formatted for some plot styles. See `model/helpers.py` for code that turns the raw data into more plotting friendly forms.\n", - "\n", - "## Customer Queue Sizes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "535f2e55", - "metadata": {}, - "outputs": [], - "source": [ - "for name, grp in df[(df[\"Entity Type\"]==\"CheckoutLane\")].groupby(\"Entity Name\"):\n", - " stepped = to_step(\n", - " [\n", - " (row[\"Time\"], row[\"Value\"])\n", - " for _, row in grp.iterrows()\n", - " if row[\"State Name\"] == \"customer_queue\"\n", - " ],\n", - " last_time=df.Time.max(),\n", - " )\n", - " dfgrp = pd.DataFrame(stepped, columns=[\"Time\", \"Customers Waiting\"])\n", - " fig = px.line(dfgrp, x='Time', y=\"Customers Waiting\")\n", - " fig.update_layout(title_text=f\"{name} Data\", )\n", - " fig.show()" - ] - }, - { - "cell_type": "markdown", - "id": "c3b47755", - "metadata": {}, - "source": [ - "## Cashier Task Timeline\n", - "\n", - "Using a `State` that is recording and stores a string is a great way to easily get activity data over time for your actors. This is highly recommended for quicker debugging and model verification.\n", - "\n", - "In this model, `current_task` is the `State`, and we process the state date into a gantt chart form for Plotly." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "770db8f6", - "metadata": {}, - "outputs": [], - "source": [ - "g1 = doing_to_gantt(df, \"Bob\", \"current_task\")\n", - "g2 = doing_to_gantt(df, \"Ertha\", \"current_task\")\n", - "g1[\"Cashier\"] = \"Bob\"\n", - "g2[\"Cashier\"] = \"Ertha\"\n", - "g = pd.concat((g1, g2))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8076b73a", - "metadata": {}, - "outputs": [], - "source": [ - "fig = px.timeline(\n", - " g, facet_col=\"Cashier\", x_start=\"Start\", x_end=\"Finish\", y=\"Task\", color=\"Task\",\n", - " facet_col_wrap=1, facet_row_spacing=0.1, height=600)\n", - "fig.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/jupyterlite/content/model/__init__.py b/jupyterlite/content/model/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/jupyterlite/content/model/cashier_model.py b/jupyterlite/content/model/cashier_model.py deleted file mode 100644 index baa51e1..0000000 --- a/jupyterlite/content/model/cashier_model.py +++ /dev/null @@ -1,296 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for license terms. - -from collections.abc import Generator -from typing import Any - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.task import InterruptStates -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -BREAK_TIME = 15.0 - -class Cashier(UP.Actor): - scan_speed = UP.State[float]( - valid_types=(float,), - frozen=True, - ) - time_until_break = UP.State[float]( - default=120.0, - valid_types=(float,), - frozen=True, - ) - breaks_until_done = UP.State[int](default=2, valid_types=int) - breaks_taken = UP.State[int](default=0, valid_types=int, recording=True) - items_scanned = UP.State[int]( - default=0, - valid_types=(int,), - recording=True, - ) - time_scanning = UP.LinearChangingState( - default=0.0, - valid_types=(float,), - ) - messages = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, - ) - current_task = UP.State[str](default="init", recording=True) - - def time_left_to_break(self) -> float: - elapsed = self.env.now - float(self.get_knowledge("start_time", must_exist=True)) - return self.time_until_break - elapsed - - -class CheckoutLane(UP.Actor): - customer_queue = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, - ) - - -class StoreBoss(UP.UpstageBase): - def __init__(self, lanes: list[CheckoutLane]) -> None: - self.lanes = lanes - self._lane_map: dict[CheckoutLane, Cashier] = {} - - def get_lane(self, cashier: Cashier) -> CheckoutLane: - possible = [lane for lane in self.lanes if lane not in self._lane_map] - lane = self.stage.random.choice(possible) - self._lane_map[lane] = cashier - return lane - - def clear_lane(self, cashier: Cashier) -> None: - to_del = [name for name, cash in self._lane_map.items() if cash is cashier] - for name in to_del: - del self._lane_map[name] - - -class CashierBreakTimer(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - times = [self.env.now + actor.time_until_break*b for b in range(1, actor.breaks_until_done+1)] - for t in times: - yield UP.Wait(t - self.env.now) - actor.interrupt_network("CashierJob", cause=dict(reason="BREAK TIME")) - - -class InterruptibleTask(UP.Task): - def on_interrupt(self, *, actor: Cashier, cause: dict[str, Any]) -> InterruptStates: - # We will only interrupt with a dictionary of data - assert isinstance(cause, dict) - job_list: list[str] - - if cause["reason"] == "BREAK TIME": - job_list = ["Break"] - elif cause["reason"] == "NEW JOB": - job_list = cause["job_list"] - else: - raise UP.SimulationError("Unexpected interrupt cause") - - # determine time until break - time_left = actor.time_left_to_break() - # if there are only five minutes left, take the break and queue the task. - if time_left <= 5.0 and "Break" not in job_list: - job_list = ["Break"] + job_list - - # Ignore the interrupt, unless we've marked it to know otherwise - marker = self.get_marker() or "none" - if marker == "on break" and "Break" in job_list: - job_list.remove("Break") - - self.clear_actor_task_queue(actor) - self.set_actor_task_queue(actor, job_list) - if marker == "cancellable": - return self.INTERRUPT.END - return self.INTERRUPT.IGNORE - - -class GoToWork(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Go to work""" - actor.current_task = "Going to Work" - yield UP.Wait(15.0) - - -class TalkToBoss(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Zero-time task to get information.""" - actor.current_task = "Talking to Boss" - boss: StoreBoss = self.stage.boss - lane = boss.get_lane(actor) - self.set_actor_knowledge(actor, "checkout_lane", lane, overwrite=False) - actor.breaks_taken = 0 - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - # Convenient spot to run the timer. - CashierBreakTimer().run(actor=actor) - - -class WaitInLane(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Wait until break time, or a customer.""" - actor.current_task = "Waiting for Customer" - lane: CheckoutLane = self.get_actor_knowledge( - actor, - "checkout_lane", - must_exist=True, - ) - customer_arrival = UP.Get(lane.customer_queue) - - self.set_marker(marker="cancellable") - yield customer_arrival - - customer: int = customer_arrival.get_value() - self.set_actor_knowledge(actor, "customer", customer, overwrite=True) - - -class DoCheckout(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Do the checkout""" - actor.current_task = "Checking out a Customer" - items: int = self.get_actor_knowledge( - actor, - "customer", - must_exist=True, - ) - per_item_time = actor.scan_speed / items - actor.activate_linear_state( - state="time_scanning", - rate=1.0, - task=self, - ) - for _ in range(items): - yield UP.Wait(per_item_time) - actor.items_scanned += 1 - actor.deactivate_all_states(task=self) - # assume 2 minutes to take payment - yield UP.Wait(2.0) - - -class Break(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Decide what kind of break we are taking.""" - actor.breaks_taken += 1 - - # we might have jobs queued - queue = self.get_actor_task_queue(actor) or [] - if "Break" in queue: - raise UP.SimulationError("Odd task network state") - self.clear_actor_task_queue(actor) - - if actor.breaks_taken == actor.breaks_until_done: - self.set_actor_task_queue(actor, ["NightBreak"]) - elif actor.breaks_taken > actor.breaks_until_done: - raise UP.SimulationError("Too many breaks taken") - else: - self.set_actor_task_queue(actor, ["ShortBreak"] + queue) - - -class ShortBreak(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Take a short break.""" - actor.current_task = "On Short Break" - self.set_marker("on break") - yield UP.Wait(BREAK_TIME) - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - - -class NightBreak(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Go home and rest.""" - actor.current_task = "Home for the Night" - self.clear_actor_knowledge(actor, "checkout_lane") - self.stage.boss.clear_lane(actor) - yield UP.Wait(60 * 12.0) - - -class Restock(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Restock.""" - actor.current_task = "Restock" - self.set_marker("quick task") - yield UP.Wait(10.0) - - -task_classes = { - "GoToWork": GoToWork, - "TalkToBoss": TalkToBoss, - "WaitInLane": WaitInLane, - "DoCheckout": DoCheckout, - "Break": Break, - "ShortBreak": ShortBreak, - "NightBreak": NightBreak, - "Restock": Restock, -} - -task_links = { - "GoToWork": UP.TaskLinks(default="TalkToBoss", allowed=["TalkToBoss"]), - "TalkToBoss": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "WaitInLane": UP.TaskLinks(default="DoCheckout", allowed=["DoCheckout", "Break"]), - "DoCheckout": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane", "Break"]), - "Break": UP.TaskLinks(default="ShortBreak", allowed=["ShortBreak", "NightBreak"]), - "ShortBreak": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "NightBreak": UP.TaskLinks(default="GoToWork", allowed=["GoToWork"]), - "Restock": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane", "Break"]), -} - -cashier_task_network = UP.TaskNetworkFactory( - name="CashierJob", - task_classes=task_classes, - task_links=task_links, -) - - -class CashierMessages(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - getter = UP.Get(actor.messages) - yield getter - tasks_needed: list[str] | str = getter.get_value() - tasks_needed = [tasks_needed] if isinstance(tasks_needed, str) else tasks_needed - actor.interrupt_network("CashierJob", cause=dict(reason="NEW JOB", job_list=tasks_needed)) - - -cashier_message_net = UP.TaskNetworkFactory.from_single_looping("Messages", CashierMessages) - - -def customer_spawner( - env: SIM.Environment, - lanes: list[CheckoutLane], - max_wait: float = 30.0, -) -> Generator[SIM.Event, None, None]: - # sneaky way to get access to stage - stage = lanes[0].stage - t_until = (8*60+1) - env.now - t_until = max(t_until, 0.0) - yield env.timeout(t_until) - while True: - hrs = env.now / 60 - days = hrs // 24 - time_of_day = hrs % 24 - if time_of_day >= 18.5: - time_at_open = 24 * (days + 1) + 8 - mins_to_open = (time_at_open - hrs) * 60 - yield env.timeout(mins_to_open) - - lane_pick = stage.random.choice(lanes) - number_pick = stage.random.randint(3, 17) - yield lane_pick.customer_queue.put(number_pick) - yield UP.Wait.from_random_uniform(5.0, max_wait).as_event() - - -def manager_process(boss: StoreBoss, cashiers: list[Cashier]) -> SIMPY_GEN: - while True: - # Use the random uniform feature, but convert the UPSTAGE event to simpy - # because this is a simpy only process - yield UP.Wait.from_random_uniform(30.0, 90.0).as_event() - possible = [ - cash - for cash in cashiers - if getattr(cash.get_running_task("CashierJob"), "name", "") != "NightBreak" - ] - if not possible: - return - cash = boss.stage.random.choice(possible) - yield cash.messages.put(["Restock"]) diff --git a/jupyterlite/content/model/helpers.py b/jupyterlite/content/model/helpers.py deleted file mode 100644 index 0705575..0000000 --- a/jupyterlite/content/model/helpers.py +++ /dev/null @@ -1,39 +0,0 @@ -# Helper functions for processing state data -import pandas as pd -from datetime import datetime, timedelta - -def to_changes(data: list[tuple[datetime, str]], excess=10.0) -> list[tuple[str, datetime, datetime]]: - start_stop_data = [] - for i in range(len(data) - 1): - start_time, start_value = data[i] - end_time, _ = data[i + 1] - start_stop_data.append((start_value, start_time, end_time)) - start_stop_data.append((data[-1][1], data[-1][0], data[-1][0] + timedelta(minutes=excess))) - return start_stop_data - - -def to_step(data: list[tuple[float, float | int]], last_time=None) -> list[tuple[float, float | int]]: - """Return data as (time, value) pairs that accounts for step-like nature of DES data.""" - step_data = [] - for i in range(len(data) - 1): - start_time, start_value = data[i] - end_time, end_value = data[i + 1] - step_data.append((start_time, start_value)) - if start_value != end_value: - step_data.append((end_time, start_value)) - # Add the last data point - step_data.append(data[-1]) - if last_time is not None: - step_data.append((last_time, step_data[-1][-1])) - return step_data - - -def doing_to_gantt(df: pd.DataFrame, actor: str, state: str) -> pd.DataFrame: - use = df[(df["Entity Name"] == actor) & (df["State Name"]==state)].sort_values("Time") - the_data = [(row["Time"], row["Value"]) for _, row in use.iterrows()] - formatted_data = to_changes(the_data) - _times = [ - dict(TaskNum=f"Task {i}", Start=start, Finish=finish, Task=value) - for i, (value, start, finish) in enumerate(formatted_data) - ] - return pd.DataFrame(_times) diff --git a/jupyterlite/requirements.txt b/jupyterlite/requirements.txt deleted file mode 100644 index 3ffaacd..0000000 --- a/jupyterlite/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -jupyterlite-core==0.6.1 -jupyterlab~=4.4.3 -notebook~=7.4.3 -jupyterlite-pyodide-kernel==0.6.1 -jupyterlab-night -plotly>=6,<7 -nbformat -upstage_des>=0.4,<0.5 diff --git a/src/upstage_des/__init__.py b/src/upstage_des/__init__.py deleted file mode 100644 index db4a761..0000000 --- a/src/upstage_des/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""A framework for modeling and simulating complex systems of systems. - -UPSTAGE (i.e., the Universal Platform for Simulating Tasks and Actors with -Graphs and Events) is built atop of SimPy, with the intent of simplifying the -development process for complex simulations. - -""" - -from ._version import __authors__, __version__ - -__all__ = ("__authors__", "__version__") diff --git a/src/upstage_des/_version.py b/src/upstage_des/_version.py deleted file mode 100644 index eac7991..0000000 --- a/src/upstage_des/_version.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Declare the UPSTAGE version.""" - -__authors__ = "UPSTAGE Contributors, GTRI" -__version__ = "0.4.0" diff --git a/src/upstage_des/actor.py b/src/upstage_des/actor.py deleted file mode 100644 index 9cb8282..0000000 --- a/src/upstage_des/actor.py +++ /dev/null @@ -1,1248 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""This file contains the fundamental Actor class for UPSTAGE.""" - -from collections import Counter, defaultdict -from collections.abc import Callable, Iterable -from copy import copy, deepcopy -from dataclasses import dataclass -from inspect import Parameter, signature -from typing import TYPE_CHECKING, Any, Self, Union - -from simpy import Process - -from upstage_des.events import Event - -from .base import ( - SPECIAL_ENTITY_CONTEXT_VAR, - MockEnvironment, - NamedUpstageEntity, - SettableEnv, - SimulationError, - UpstageError, -) -from .data_types import CartesianLocation, GeodeticLocation -from .states import ( - ActiveState, - CartesianLocationChangingState, - DetectabilityState, - GeodeticLocationChangingState, - ResourceState, - State, - _KeyValueBase, -) -from .task import Task -from .task_network import TaskNetwork, TaskNetworkFactory -from .utils import get_caller_info, get_caller_object - -__all__ = ("Actor",) - -if TYPE_CHECKING: - from .nucleus import TaskNetworkNucleus - -LOC_STATE = GeodeticLocationChangingState | CartesianLocationChangingState -LOCATIONS = GeodeticLocation | CartesianLocation -LOC_LIST = list[GeodeticLocation] | list[CartesianLocation] - - -@dataclass -class TaskData: - name: str - process: Process - - -class Actor(SettableEnv, NamedUpstageEntity): - """Actors perform tasks and are composed of states. - - You can subclass, but do not overwrite __init_subclass__. Mixins are allowed - but they cannot depend on __init__. Always put mixins after actor base classes. - """ - - def __init_states(self, **states: Any) -> None: - seen = set() - for state, value in states.items(): - if state in self._state_defs: - seen.add(state) - st = self._state_defs[state] - if st._no_init: - raise SimulationError( - f"State {state} on {self} has set no_init=True. " - "Initializing a no_init state is disallowed." - ) - setattr(self, state, value) - else: - raise UpstageError(f"Input to {self} was not expected: {state}={value}") - exist = set(self._state_defs.keys()) - unseen = exist - seen - for state_name in unseen: - _state = self._state_defs[state_name] - if _state.has_default(): - seen.add(state_name) - _state._set_default(self) - if len(seen) != len(exist): - raise UpstageError( - f"Missing values for states! These states need values: " - f"{exist - seen} to be specified for '{self.name}'." - ) - if "log" in seen: - raise UpstageError("Do not name a state `log`") - # Check that we won't name clash state names and recording function names - recording_names: dict[str, int] = Counter() - for name, state_def in self._state_defs.items(): - recording_names[name] += 1 - for _, rec_name in state_def._recording_functions: - recording_names[rec_name] += 1 - error_msg = "" - for k, v in recording_names.items(): - if v > 1: - error_msg += f"Duplicated state or recording name: {k}\n" - if error_msg: - raise SimulationError(error_msg) - - def __actual_init__( - self, - *, - name: str, - debug_log: bool = True, - debug_log_time: bool | None = None, - initial_knowledge: dict[str, Any] | None = None, - **states: Any, - ) -> None: - """Create an Actor. - - Args: - name (str): The name of the actor. - debug_log (bool, optional): Whether to write to debug log. Defaults to True. - debug_log_time (bool, optional): If time is logged in debug messages. - Defaults to None (uses Stage value), otherwise local value is used. - initial_knowledge (dict[str, Any], optional): A dictionary to initialize - knowledge with. - states (Any): Values for each state as kwargs. - """ - self.name = name - super().__init__() - - self._active_states: dict[str, dict[str, Any]] = {} - self._num_clones: int = 0 - self._state_defs: dict[str, State] = getattr(self.__class__, "_state_defs", {}) - - self._mimic_states: dict[str, tuple[Actor, str]] = {} # has to be before other calls - self._mimic_states_by_task: dict[Task, set[str]] = defaultdict(set) - - self._states_by_task: dict[Task, set[str]] = defaultdict(set) - self._tasks_by_state: dict[str, set[Task]] = defaultdict(set) - - self._task_networks: dict[str, TaskNetwork] = {} - self._task_queue: dict[str, list[str]] = {} - - self._knowledge: dict[str, Any] = {} - self._is_rehearsing: bool = False - - self._debug_logging: bool = debug_log - self._debug_log_time = debug_log_time - self._debug_log: list[tuple[float | int, str]] = [] - - self._state_histories: dict[str, list[tuple[float, Any]]] = {} - - # Task Network Nucleus hook-ins - self._state_listener: TaskNetworkNucleus | None = None - - self.__init_states(**states) - - if initial_knowledge is not None: - self.set_bulk_knowledge(initial_knowledge, overwrite=True, caller="init") - - def __init__( - self, - *, - name: str, - debug_log: bool = True, - debug_log_time: bool | None = None, - initial_knowledge: dict[str, Any] | None = None, - **states: Any, - ) -> None: - """Create an actor. - - This specific version of __init__ exists to be overriden. - - Args: - name (str): The name of the actor. - debug_log (bool, optional): Whether to write to debug log. Defaults to True. - debug_log_time (bool, optional): If time is logged in debug messages. - Defaults to None (uses Stage value), otherwise local value is used. - initial_knowledge (dict[str, Any], optional): A dictionary to initialize - knowledge with. - states (Any): Keyword args. - """ - self.__actual_init__( - name=name, - debug_log=debug_log, - debug_log_time=debug_log_time, - initial_knowledge=initial_knowledge, - ) - - def __init_subclass__( - cls, - *args: Any, - **kwargs: Any, - ) -> None: - super().__init_subclass__(*args, **kwargs) - all_states: dict[str, State] = {} - # This ensures that newer classes overwrite older states - for base_class in cls.mro()[::-1]: - for state_name, state in base_class.__dict__.items(): - if isinstance(state, State): - if state_name in all_states: - raise ValueError(f"Duplicated state name: {state_name}") - all_states[state_name] = state - state.name = state_name - cls._state_defs = all_states - - nxt = cls.mro()[1] - if nxt is object: - raise UpstageError(f"Actor has bad inheritance, MRO: {cls.mro()}") - - def new_init( - self: Actor, - *, - name: str, - debug_log: bool = True, - debug_log_time: bool | None = None, - **states: Any, - ) -> None: - self.__actual_init__( - name=name, debug_log=debug_log, debug_log_time=debug_log_time, **states - ) - - # Update the docstring - might be helpful for active doc builds - docstring = f"""Create a {cls.__name__} actor. - -Args: - name (str): The name of the actor. - debug_log (bool, optional): Whether to write to debug log. Defaults to True. - debug_log_time (bool, optional): If time is logged in debug messages. - Defaults to None (uses Stage value), otherwise local value is used. -""" - sig = signature(new_init) - params = list(sig.parameters.values()) - # Find the "states=" parameter of the signature and remove it. - state_parameter = [x for x in params if x.name == "states"] - if state_parameter: - params.remove(state_parameter[0]) - for state, value in all_states.items(): - if value._no_init: - continue - typ = Any if not value._types else Union[*value._types] - default_str = "No Default" - default: Any = Parameter.empty - if value.has_default(): - default = value._default if value._default is not None else value._default_factory - default_str = f"{default}" - params.insert( - len(params), - Parameter(state, Parameter.KEYWORD_ONLY, annotation=typ, default=default), - ) - docstring += f"\n {state} ({type}): Actor State. Defaults to {default_str}." - try: - setattr(new_init, "__signature__", sig.replace(parameters=params)) - except ValueError as e: - e.add_note(f"Failure likely due to repeated state name in inherited actor {cls}") - raise e - new_init.__doc__ = docstring - setattr(cls, "__init__", new_init) - - def _add_special_group(self) -> None: - """Add self to the actor context list. - - Called by the NamedUpstageEntity on group inits. - """ - ans = SPECIAL_ENTITY_CONTEXT_VAR.get().actors - if self in ans: - return - ans.append(self) - - def _lock_state(self, *, state: str, task: Task) -> None: - """Lock one of the actor's states by a given task. - - Args: - state (str): The name of the state to lock - task (Task): The task that is locking the state - """ - the_state = self._state_defs[state] - if not the_state.IGNORE_LOCK: - # single-task only, so no task should - # be associated with this state - if self._tasks_by_state[state]: - raise SimulationError( - f"State '{state}' cannot be used by '{task}' because it is " - f"locked by {self._tasks_by_state[state]}" - ) - else: - # We can have multiple locks, but make sure we are repeating a lock - if task in self._tasks_by_state[state]: - raise SimulationError( - f"State '{state}' already locked by '{task}'. " - "Did you forget to unlock/deactivate it?" - ) - self._states_by_task[task].add(state) - self._tasks_by_state[state].add(task) - - def _set_active_state_data( - self, - state_name: str, - started_at: float | None = None, - **data: Any, - ) -> None: - """Set the data for an active state. - - Args: - state_name (str): Name of the state - started_at (Optional[float], optional): Time the data is set at. Defaults to None. - **data (Any): key:values as kwargs for the state data. - """ - # Rule: underscored active data will get remembered - started_at = self.env.now if started_at is None else started_at - old_data = self._active_states.get(state_name, {}) - new_data = {"started_at": started_at, **data} - keep_old = {k: v for k, v in old_data.items() if k not in new_data and "_" == k[0]} - new_data.update(keep_old) - self._active_states[state_name] = new_data - - def activate_state( - self, - *, - state: str, - task: Task, - **kwargs: Any, - ) -> None: - """Set a state as active. - - Note: - This method is used by the tasks for activating states they use/modify. - - TODO: on init, create `activate_` methods that type-hint the inputs - - Args: - state (str): The name of the state to set as active. - task (Task): The task that is activating the state. - **kwargs (Any): key:values as kwargs for the state activation. - """ - if state not in self._state_defs: - raise SimulationError(f"No state named '{state}' to activate") - self._lock_state(state=state, task=task) - self._set_active_state_data(state_name=state, started_at=self.env.now, task=task, **kwargs) - # any initialization in the state needs to be called via attribute access - getattr(self, state) - # The activation handles getattr - _state = self._state_defs[state] - assert isinstance(_state, ActiveState) - _state.activate(self, task=task) - - def activate_linear_state(self, *, state: str, rate: float, task: Task) -> None: - """Shortcut for activating a LinearChangingState. - - Args: - state (str): The name of the LinearChangingState to set as active. - rate (float): The rate of the change - task (Task): The task that is activating - """ - self.activate_state(state=state, task=task, rate=rate) - - def activate_location_state( - self, *, state: str, speed: float, waypoints: LOC_LIST, task: Task - ) -> None: - """Shortcut for activating a (Cartesian|Geodetic)LocationChangingState. - - Args: - state (str): State name - speed (float): The speed to move at - waypoints (LOC_LIST): Waypoints to move over - task (Task): The task that the state is activated during. - """ - self.activate_state( - state=state, - speed=speed, - waypoints=waypoints, - task=task, - ) - - def _unlock_state(self, *, state: str, task: Task) -> None: - """Release a task's lock of a state. - - Args: - state (str): The name of the state to lock - task (Task): The task that is locking the state - """ - the_state = self._state_defs[state] - if not the_state.IGNORE_LOCK: - # single-task only, so only one task should - # be associated with this state - if task not in self._tasks_by_state[state]: - raise SimulationError( - f"State `{state}` isn't locked by '{task}', but it's trying to be unlocked." - ) - self._states_by_task[task].remove(state) - self._tasks_by_state[state].remove(task) - elif task in self._tasks_by_state[state]: - self._states_by_task[task].remove(state) - self._tasks_by_state[state].remove(task) - else: - raise UpstageError(f"State '{state}' was not activated by '{task}', cannot deactivate") - - def deactivate_states(self, *, states: str | Iterable[str], task: Task) -> None: - """Set a list of active states to not active. - - Args: - states (str | Iterable[str]): The names of the states to deactivate. - task (Task): The task that is deactivating the states. - """ - if isinstance(states, str): - states = [states] - - for state in states: - self.deactivate_state(state=state, task=task) - - def deactivate_state(self, *, state: str, task: Task) -> None: - """Deactivate a specific state. - - Args: - state (str): The name of the state to deactivate. - task (Task): The task that is deactivating the state. - """ - self._unlock_state(state=state, task=task) - - # the deactivated state may need to be updated - getattr(self, state) - # and then deactivate it, only if it was unlocked - the_state = self._state_defs[state] - if not isinstance(the_state, ActiveState): - raise UpstageError(f"Stage {state} is not an active type state.") - ignore = the_state.deactivate(self, task=task) - if state in self._active_states and not ignore: - del self._active_states[state] - - def deactivate_all_states(self, *, task: Task) -> None: - """Deactivate all states in the actor for a given task. - - Args: - task (Task): The task that is deactivating the states. - """ - state_names = list(self._states_by_task[task]) - self.deactivate_states(states=state_names, task=task) - - def get_active_state_data( - self, state_name: str, without_update: bool = False - ) -> dict[str, Any]: - """Get the data for a specific state. - - Args: - state_name (str): The name of the state for which to retrieve the data. - without_update (bool): Whether or not to update the state to the current - sim time. Defaults to True - - Returns: - dict[str, Any]: The state data. - """ - if not without_update: - getattr(self, state_name) - ans: dict[str, Any] = self._active_states.get(state_name, {}) - return ans - - def _mimic_state_name(self, self_state: str) -> str: - """Create a mimic state name. - - Args: - self_state (str): The name of the state - - Returns: - str: Mimic-safe name - """ - return f"{id(self)}-{self_state}" - - def activate_mimic_state( - self, - *, - self_state: str, - mimic_state: str, - mimic_actor: "Actor", - task: Task, - ) -> None: - """Activate a state to mimic a state on another actor. - - Args: - self_state (str): State name to be the mimic - mimic_state (str): State on the other actor to be mimiced - mimic_actor (Actor): The other actor. - task (Task): The task during which the state is mimiced. - """ - caller = get_caller_object() - if isinstance(caller, Task) and caller._rehearsing: - raise UpstageError("Mimic state activated on rehearsal. This is unsupported/unstable") - if self_state in self._mimic_states: - raise UpstageError(f"{self_state} already mimicked") - - state = self._state_defs[self_state] - if isinstance(state, _KeyValueBase): - raise SimulationError(f"States of type {type(state)} are not mimic-able.") - if isinstance(mimic_actor._state_defs[mimic_state], _KeyValueBase): - raise SimulationError( - f"States of type {type(mimic_actor._state_defs[mimic_state])} are not mimic-able." - ) - their_v = getattr(mimic_actor, mimic_state) - if not state._type_check(their_v, throw=False): - raise SimulationError( - f"Cannot mimic states of different types: {state._types} vs {type(their_v)}" - ) - - self._mimic_states[self_state] = (mimic_actor, mimic_state) - self._mimic_states_by_task[task].add(self_state) - - self_state_name = self._mimic_state_name(self_state) - if state.is_recording: - - def recorder(instance: Actor, value: Any) -> None: - if instance is mimic_actor: - state._do_record(self, value) - - mimic_actor._add_callback_to_state(self_state_name, recorder, mimic_state) - - def deactivate_mimic_state(self, *, self_state: str, task: Task) -> None: - """Deactivate a mimicking state. - - Args: - self_state (str): State name - task (Task): Task it's running in. - """ - getattr(self, self_state) - mimic_actor, mimic_state = self._mimic_states[self_state] - state = self._state_defs[self_state] - self_state_name = self._mimic_state_name(self_state) - if state.is_recording: - mimic_actor._remove_callback_from_state(self_state_name, mimic_state) - del self._mimic_states[self_state] - self._mimic_states_by_task[task].remove(self_state) - - def deactivate_all_mimic_states(self, *, task: Task) -> None: - """Deactivate all mimicking states in the actor for a given task. - - Args: - task (Task): The task where states are mimicking others. - """ - for state in list(self._mimic_states): - self.deactivate_mimic_state(self_state=state, task=task) - - def _add_callback_to_state( - self, - source: Any, - callback: Callable[["Actor", Any], Any], - state_name: str, - ) -> None: - """Add a callback to a state for recording. - - Args: - source (Any): The source for keying the callback (unused, but for the key) - callback (Callable[[Actor, Any], Any]): Takes the actor and state value - state_name (str): _description_ - """ - state: State = self._state_defs[state_name] - state._add_callback(source, callback) - - def _remove_callback_from_state( - self, - source: Any, - state_name: str, - ) -> None: - """Remove a state callback based on the source key. - - Args: - source (Any): Callback key - state_name (str): Name of the state with the callback. - """ - state = self._state_defs[state_name] - state._remove_callback(source) - - def get_knowledge(self, name: str, must_exist: bool = False) -> Any: - """Get a knowledge value from the actor. - - Args: - name (str): The name of the knowledge - must_exist (bool): Raise an error if the knowledge isn't present. Defaults to false. - - Returns: - Any: The knowledge value. None if the name doesn't exist. - """ - if must_exist and name not in self._knowledge: - raise SimulationError(f"Knowledge '{name}' does not exist in {self}") - return self._knowledge.get(name, None) - - def get_and_clear_knowledge(self, name: str, caller: str | None = None) -> Any: - """Get knowledge and clear it. - - Clearing knowledge implies it must exist in the direct methods, so the - same assumption holds here. - - Args: - name (str): Knowledge name. - caller (str): The name of the calling process for logging. Defaults to None. - - Returns: - Any: The knowledge value. - """ - know = self.get_knowledge(name, must_exist=True) - self.clear_knowledge(name, caller) - return know - - def _log_caller( - self, - method_name: str = "", - caller_level: int = 1, - caller_name: str | None = None, - ) -> None: - """Log information about who is calling this method. - - If no caller_name is given, it is searched for in the stack. - - Args: - method_name (str, optional): Method name for logging. Defaults to "". - caller_level (int, optional): Level to look up for the caller. Defaults to 1. - caller_name (Optional[str], optional): Name of the caller. Defaults to None. - """ - if caller_name is None: - info = get_caller_info(caller_level=caller_level + 1) - else: - info = caller_name - self.log(f"method '{method_name}' called by '{info}'") - - def set_knowledge( - self, - name: str, - value: Any, - overwrite: bool = False, - caller: str | None = None, - ) -> None: - """Set a knowledge value. - - Raises an error if the knowledge exists and overwrite is False. - - Args: - name (str): The name of the knowledge item. - value (Any): The value to store for the knowledge. - overwrite (bool, Optional): Allow the knowledge to be changed if it exits. - Defaults to False. - caller (str, Optional): The name of the object that called the method. - """ - self._log_caller(f"set_knowledge '{name}={value}'", caller_name=caller) - if name in self._knowledge and not overwrite: - raise SimulationError( - f"Actor {self} overwriting existing knowledge {name} " - f"without override permission. \n" - f"Current: {self._knowledge[name]}, New: {value}" - ) - else: - self._knowledge[name] = value - - def clear_knowledge(self, name: str, caller: str | None = None) -> None: - """Clear a knowledge value. - - Raises an error if the knowledge does not exist. - - Args: - name (str): The name of the knowledge item to clear. - caller (str): The name of the Task that called the method. - Used for debug logging purposes. - - """ - self._log_caller(f"clear_knowledge '{name}'", caller_name=caller) - if name not in self._knowledge: - raise SimulationError(f"Actor {self} does not have knowledge: {name}") - else: - del self._knowledge[name] - - def set_bulk_knowledge( - self, know: dict[str, Any], overwrite: bool = False, caller: str | None = None - ) -> None: - """Set multiple knowledge entries at once. - - Args: - know (dict[str, Any]): Dictionary of key:value pairs of knowledge. - overwrite (bool, optional): If overwrite is allowed. Defaults to False. - caller (str | None, optional): The name of the Task that called the method. - Defaults to None. - """ - for k, v in know.items(): - self.set_knowledge(k, v, overwrite, caller) - - def clear_bulk_knowledge(self, names: Iterable[str], caller: str | None = None) -> None: - """Clear a list of knowledge entries. - - Args: - names (Iterable[str]): Knowledge names. - caller (str | None, optional): The name of the Task that called the method. - Defaults to None. - """ - for name in names: - self.clear_knowledge(name, caller) - - def get_bulk_knowledge(self, names: Iterable[str], must_exist: bool = False) -> dict[str, Any]: - """Get multiple knowledge items. - - Args: - names (Iterable[str]): Names of the knowledge - must_exist (bool, optional): If all entires must exist. Defaults to False. - - Returns: - dict[str, Any]: The knowledge values. None if not present. - """ - return {name: self.get_knowledge(name, must_exist) for name in names} - - def get_and_clear_bulk_knowledge( - self, names: Iterable[str], caller: str | None = None - ) -> dict[str, Any]: - """Get and clear multiple knowledge entries. - - Args: - names (Iterable[str]): The knowledge to retrieve and delete. - caller (str | None, optional): The name of the caller. Defaults to None. - - Returns: - dict[str, Any]: The retrieved knowledge. - """ - return {name: self.get_and_clear_knowledge(name, caller) for name in names} - - def add_task_network(self, network: TaskNetwork) -> None: - """Add a task network to the actor. - - Args: - network (TaskNetwork): The task network to add to the actor. - """ - network_name = network.name - if network_name in self._task_networks: - raise SimulationError(f"Task network{network_name} already in {self}") - self._task_networks[network_name] = network - self._task_queue[network_name] = [] - - def clear_task_queue(self, network_name: str) -> None: - """Empty the actor's task queue. - - This will cause the task network to be used for task flow. - - Args: - network_name (str): The name of the network to clear the task queue. - """ - self._log_caller("clear_task_queue") - self._task_queue[network_name] = [] - - def set_task_queue(self, network_name: str, task_list: list[str]) -> None: - """Initialize an actor's empty task queue. - - Args: - network_name (str): Task Network name - task_list (list[str]): List of task names to queue. - - Raises: - SimulationError: _description_ - """ - self._log_caller("set_task_queue") - if self._task_queue[network_name]: - raise SimulationError(f"Task queue on {self.name} is already set. Use append or clear.") - self._task_queue[network_name] = list(task_list) - - def get_task_queue(self, network_name: str) -> list[str]: - """Get the actor's task queue on a single network. - - Args: - network_name (str): The network name - - Returns: - list[str]: List of task names in the queue - """ - return self._task_queue[network_name] - - def get_all_task_queues(self) -> dict[str, list[str]]: - """Get the task queues for all running networks. - - Returns: - dict[str, list[str]]: Task names, keyed on task network name. - """ - queues: dict[str, list[str]] = {} - for name in self._task_networks.keys(): - queues[name] = self.get_task_queue(name) - return queues - - def get_next_task(self, network_name: str) -> None | str: - """Return the next task the actor has been told if there is one. - - This does not clear the task, it's information only. - - Args: - network_name (str): The name of the network - - Returns: - None | str: The name of the next task, None if no next task. - """ - queue = self._task_queue[network_name] - queue_length = len(queue) - return None if queue_length == 0 else queue[0] - - def _clear_task(self, network_name: str) -> None: - """Clear a task from the queue. - - Useful for rehearsal. - """ - self._task_queue[network_name].pop(0) - - def _begin_next_task(self, network_name: str, task_name: str) -> None: - """Clear the first task in the task queue. - - The task name is required to check that the next task follows the actor's plan. - - Args: - network_name (str): The task network name - task_name (str): The name of the task to start - """ - queue = self._task_queue.get(network_name) - if queue and queue[0] != task_name: - raise SimulationError( - f"Actor {self.name} commanded to perform '{task_name}' but '{queue[0]}' is expected" - ) - elif not queue: - self.set_task_queue(network_name, [task_name]) - self.log(f"begin_next_task: Starting {task_name} task") - self._task_queue[network_name].pop(0) - - def start_network_loop( - self, - network_name: str, - init_task_name: str | None = None, - ) -> None: - """Start a task network looping/running on an actor. - - If no task name is given, it will default to following the queue. - - Args: - network_name (str): Network name. - init_task_name (str, optional): Task to start with. Defaults to None. - """ - network = self._task_networks[network_name] - network.loop(actor=self, init_task_name=init_task_name) - - def get_running_task(self, network_name: str) -> TaskData | None: - """Return name and process reference of a task on this Actor's task network. - - Useful for finding a process to call interrupt() on. - - Args: - network_name (str): Network name. - - Returns: - TaskData: Dataclass of name and process for the current task. - {"name": Name, "process": the Process simpy is holding.} - """ - if network_name not in self._task_networks: - raise SimulationError(f"{self} does not have a task networked named {network_name}") - net = self._task_networks[network_name] - if net._current_task_proc is not None: - assert net._current_task_name is not None - assert net._current_task_proc is not None - task_data = TaskData(name=net._current_task_name, process=net._current_task_proc) - return task_data - return None - - def get_running_tasks(self) -> dict[str, TaskData]: - """Get all running task data. - - Returns: - dict[str, dict[str, TaskData]]: Dictionary of all running tasks. - Keyed on network name, then {"name": Name, "process": ...} - """ - tasks: dict[str, TaskData] = {} - for name, net in self._task_networks.items(): - if net._current_task_proc is not None: - assert net._current_task_name is not None - tasks[name] = TaskData(name=net._current_task_name, process=net._current_task_proc) - return tasks - - def interrupt_network(self, network_name: str, **interrupt_kwargs: Any) -> None: - """Interrupt a running task network. - - Args: - network_name (str): The name of the network. - interrupt_kwargs (Any): kwargs to pass to the interrupt. - """ - data = self.get_running_task(network_name) - if data is None: - raise UpstageError(f"No processes named {network_name} is running.") - data.process.interrupt(**interrupt_kwargs) - - def has_task_network(self, network_id: Any) -> bool: - """Test if a network id exists. - - Args: - network_id (Any): Typically a string for the network name. - - Returns: - bool: If the task network is on this actor. - """ - return network_id in self._task_networks - - def suggest_network_name(self, factory: TaskNetworkFactory) -> str: - """Deconflict names of task networks by suggesting a new name. - - Used for creating multiple parallel task networks. - - Args: - factory (TaskNetworkFactory): The factory from which you will create the network. - - Returns: - str: The network name to use - """ - new_name = factory.name - if new_name not in self._task_networks: - return new_name - i = 0 - while new_name in self._task_networks: - i += 1 - new_name = f"{factory.name}_{i}" - return new_name - - def delete_task_network(self, network_id: Any) -> None: - """Deletes a task network reference. - - Be careful, the network may still be running! - - Do any interruptions on your own. - - Args: - network_id (Any): Typically a string for the network name. - """ - if not self.has_task_network(network_id): - raise SimulationError(f"No networked with id: {network_id} to delete") - del self._task_networks[network_id] - - def rehearse_network( - self, - network_name: str, - task_name_list: list[str], - knowledge: dict[str, Any] | None = None, - end_task: str | None = None, - ) -> Self: - """Rehearse a network on this actor. - - Supply the network name, the tasks to rehearse from this state, and - any knowledge to apply to the cloned actor. - - Args: - network_name (str): Network name - task_name_list (list[str]): Tasks to rehearse on the network. - knowledge (dict[str, Any], optional): knowledge to give to the cloned - actor. Defaults to None. - end_task (str, optional): A task to end on once reached. - - Returns: - Actor: The cloned actor after rehearsing the network. - """ - knowledge = {} if knowledge is None else knowledge - net = self._task_networks[network_name] - understudy = net.rehearse_network( - actor=self, - task_name_list=task_name_list, - knowledge=knowledge, - end_task=end_task, - ) - return understudy - - def clone( - self, - new_env: MockEnvironment | None = None, - knowledge: dict[str, Any] | None = None, - **new_states: Any, - ) -> Self: - """Clones an actor and assigns it a new environment. - - Note: - This function is useful when testing if an actor can accomplish a - task. - - In general, cloned actor are referred to as ``understudy`` - to keep with the theater analogy. - - The clones' names are appended with the label ``'[CLONE #]'`` where - ``'#'`` indicates the number of clones of the actor. - - Args: - new_env (Optional[MockEnvironment], optional): Environment for cloning. - Defaults to None. - knowledge (Optional[dict[str, Any]], optional): Knowledge for the clone. - Defaults to None. - new_states (Any): New states to add to the actor when cloning. - - Returns: - Actor: The cloned actor of the same type - """ - knowledge = {} if knowledge is None else knowledge - new_env = MockEnvironment.mock(self.env) if new_env is None else new_env - - states: dict[str, Any] = {} - for state in self.states: - state_obj = self._state_defs[state] - if isinstance(state_obj, ResourceState): - states[state] = state_obj._make_clone(self, getattr(self, state)) - elif isinstance(state_obj, _KeyValueBase): - states[state] = state_obj._make_clone(self) - else: - states[state] = copy(getattr(self, state)) - states.update(new_states) - - self._num_clones += 1 - - clone = self.__class__( - name=self.name + f" [CLONE {self._num_clones}]", - debug_log=self._debug_logging, - debug_log_time=self._debug_log_time, - **states, - ) - clone.env = new_env - - ignored_attributes = list(states.keys()) + ["env", "stage"] - - for attribute_name, attribute in self.__class__.__dict__.items(): - if not any( - ( - attribute_name in ignored_attributes, - attribute_name.startswith("_"), - callable(attribute), - ) - ): - setattr(clone, attribute_name, attribute) - - # update the state histories - for state_name in self._state_defs: - if state_name in self._state_histories: - clone._state_histories[state_name] = deepcopy(self._state_histories[state_name]) - - clone._knowledge = {} - for name, data in self._knowledge.items(): - clone._knowledge[name] = copy(data) - - for name, data in knowledge.items(): - clone._knowledge[name] = copy(data) - - clone._task_queue = copy(self._task_queue) - clone._task_networks = copy(self._task_networks) - - if clone._debug_logging: - clone._debug_log = list(self._debug_log) - - clone._is_rehearsing = True - return clone - - def log(self, msg: str | None = None) -> list[tuple[float | int, str]] | None: - """Add to the log or return it. - - Only adds to log if debug_logging is True. - - Args: - msg (str, Optional): The message to log. - - Returns: - list[str] | None: The log if no message is given. None otherwise. - """ - if msg is None: - return self._debug_log - elif self._debug_logging: - dlt = self._debug_log_time - do_time = dlt if dlt is not None else self.stage.get("debug_log_time", True) - if do_time: - ts = self.pretty_now - msg = f"{ts} {msg}" - self._debug_log += [(self.env.now, msg)] - return None - - def get_log(self) -> list[tuple[float | int, str]]: - """Get the debug log. - - Returns: - list[str]: List of log messages. - """ - return self._debug_log - - @property - def states(self) -> tuple[str, ...]: - """Get the names of the actor's states. - - Returns: - tuple[str]: State names - """ - return tuple(self._state_defs.keys()) - - @property - def state_values(self) -> dict[str, Any]: - """Get the state names and values. - - Returns: - dict[str, Any]: State name:value pairs. - """ - return {k: getattr(self, k) for k in self.states} - - def _get_detection_state(self) -> None | str: - """Find the name of a state is of type DetectabilityState. - - Returns: - None | str: The name of the state (None if none found). - """ - detection = [k for k, v in self._state_defs.items() if isinstance(v, DetectabilityState)] - if len(detection) > 1: - raise NotImplementedError("Only 1 state of type DetectabilityState allowed for now") - return None if not detection else detection[0] - - def _match_attr(self, name: str) -> str | None: - """Test if self has a matching attribute name. - - Args: - name (str): The attribute to find - - Returns: - str | None: The name if it has it, None otherwise. - """ - if not hasattr(self, name): - return None - return name - - def _get_matching_state( - self, - state_class: type[State], - attr_matches: dict[str, Any] | None = None, - ) -> str | None: - """Find a state that matches the class and optional attributes and return its name. - - For multiple states with the same class, this returns the first available. - - Args: - state_class (State): The class of state to search for - attr_matches (Optional[dict[str, Any]], optional): Attributes and values - to match. Defaults to None. - - Returns: - str | None: The name of the state (for getattr) - """ - - def match_tester(nm: str, val: Any, state: State) -> bool: - if hasattr(state, nm): - matching: bool = getattr(state, nm) == val - return matching - return False - - for name, state in self._state_defs.items(): - if not isinstance(state, state_class): - continue - - if attr_matches is None: - return self._match_attr(name) - - has_attribute_matches = all( - match_tester(nm, val, state) for nm, val in attr_matches.items() - ) - if has_attribute_matches: - return self._match_attr(name) - return None - - def create_knowledge_event( - self, - *, - name: str, - rehearsal_time_to_complete: float = 0.0, - ) -> Event: - """Create an event and store it in knowledge. - - Useful for creating simple hold points in Tasks that can be succeeded by - other processes. - - Example: - >>> def task(self, actor): - >>> evt = actor.create_knowledge_event(name="hold") - >>> yield evt - >>> ... # do things - ... - >>> def other_task(self, actor): - >>> if condition: - >>> actor.succeed_knowledge_event(name="hold") - - Args: - name (str): Name of the knowledge slot to store the event in. - rehearsal_time_to_complete (float, optional): The event's expected - time to complete. Defaults to 0.0. - - Returns: - Event: The event to yield on - """ - event = Event(rehearsal_time_to_complete=rehearsal_time_to_complete) - # Rehearsals on this method won't clear the event, so save the user some trouble. - overwrite = True if self._is_rehearsing else False - self.set_knowledge(name, event, overwrite=overwrite) - return event - - def succeed_knowledge_event(self, *, name: str, **kwargs: Any) -> None: - """Succeed and clear an event stored in the actor's knowledge. - - See "create_knowledge_event" for usage example. - - Args: - name (str): Event knowledge name. - **kwargs (Any): Any payload to send to the event. Defaults to None - """ - event = self.get_knowledge(name) - if event is None: - raise SimulationError(f"No knowledge named {name} to succeed") - if not isinstance(event, Event): - raise SimulationError(f"Knowledge {name} is not an Event.") - self.clear_knowledge(name, "actor.succeed_knowledge_event") - event.succeed(**kwargs) - - def get_remaining_waypoints( - self, location_state: str - ) -> list[GeodeticLocation] | list[CartesianLocation]: - """Convenience method for interacting with LocationChangingStates. - - Primary use case is when restarting a Task that has a motion element to - allow updating waypoint knowledge easily. - - Args: - location_state (str): The name of the - - Returns: - list[Location]: List of waypoints yet to be reached - """ - loc_state = self._state_defs[location_state] - assert isinstance(loc_state, GeodeticLocationChangingState | CartesianLocationChangingState) - wypts = loc_state._get_remaining_waypoints(self) - return wypts - - def get_nucleus(self) -> "TaskNetworkNucleus": - """Return the actor's nucleus. - - Returns: - TaskNetworkNucleus: The nucleus on the actor. - """ - if self._state_listener is None: - raise SimulationError("Expected a nucleus, but none found.") - return self._state_listener - - def record_state(self, state_name: str) -> None: - """Record a state by its name. - - Useful for states that have attributes that aren't set - via the descriptor, such as dictionaries or dataclasses. - - Args: - state_name (str): The name of the state. - """ - if state_name not in self.states: - raise SimulationError(f"No state '{state_name}' to record.") - v = getattr(self, state_name) - self._state_defs[state_name]._do_record(self, v) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}: {self.name}" diff --git a/src/upstage_des/api.py b/src/upstage_des/api.py deleted file mode 100644 index 0354002..0000000 --- a/src/upstage_des/api.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""The elements in the UPSTAGE Application Programmable Interface.""" - -# Core -# Director, stage, and Exceptions -# Actor -from upstage_des.actor import Actor -from upstage_des.base import ( - EnvironmentContext, - MotionAndDetectionError, - NamedUpstageEntity, - RulesError, - SimulationError, - UpstageBase, - UpstageError, - add_stage_variable, - get_stage, - get_stage_variable, -) - -# Comms -from upstage_des.communications.comms import Message, MessageContent, PointToPointCommsManager -from upstage_des.communications.routing import RoutingTableCommsManager - -# Constants -from upstage_des.constants import PLANNING_FACTOR_OBJECT - -# Data types -from upstage_des.data_types import ( - CartesianLocation, - CartesianLocationData, - GeodeticLocation, - GeodeticLocationData, - Location, -) - -# Events -from upstage_des.events import All, Any, Event, FilterGet, Get, Put, ResourceHold, Wait - -# Motion -from upstage_des.motion import SensorMotionManager, SteppedMotionManager - -# Task network nucleus -from upstage_des.nucleus import NucleusInterrupt, TaskNetworkNucleus - -# Resources -from upstage_des.resources.container import ( - ContainerEmptyError, - ContainerError, - ContainerFullError, - ContinuousContainer, -) -from upstage_des.resources.monitoring import ( - SelfMonitoringContainer, - SelfMonitoringContinuousContainer, - SelfMonitoringFilterStore, - SelfMonitoringReserveContainer, - SelfMonitoringSortedFilterStore, - SelfMonitoringStore, -) -from upstage_des.resources.reserve import ReserveContainer -from upstage_des.resources.sorted import SortedFilterGet, SortedFilterStore - -# Routine -from upstage_des.routines import Routine, WindowedGet - -# Nucleus-friendly states -from upstage_des.state_sharing import SharedLinearChangingState - -# States -from upstage_des.states import ( - CartesianLocationChangingState, - CommunicationStore, - DataclassState, - DetectabilityState, - DictionaryState, - GeodeticLocationChangingState, - LinearChangingState, - MultiStoreState, - ResourceState, - State, -) - -# Task -from upstage_des.task import DecisionTask, InterruptStates, Task, TerminalTask, process - -# Task Networks -from upstage_des.task_network import TaskLinks, TaskNetwork, TaskNetworkFactory - -# Conversion -from upstage_des.units import unit_convert - -__all__ = [ - "UpstageError", - "SimulationError", - "MotionAndDetectionError", - "RulesError", - "Actor", - "PLANNING_FACTOR_OBJECT", - "UpstageBase", - "NamedUpstageEntity", - "EnvironmentContext", - "add_stage_variable", - "get_stage_variable", - "get_stage", - "All", - "Any", - "Event", - "Get", - "FilterGet", - "SortedFilterGet", - "Put", - "ResourceHold", - "Wait", - "ContainerEmptyError", - "ContainerError", - "ContainerFullError", - "ContinuousContainer", - "SelfMonitoringContainer", - "SelfMonitoringContinuousContainer", - "SelfMonitoringFilterStore", - "SelfMonitoringSortedFilterStore", - "SelfMonitoringReserveContainer", - "SelfMonitoringStore", - "ReserveContainer", - "SortedFilterStore", - "CartesianLocation", - "GeodeticLocation", - "Location", - "CartesianLocationData", - "GeodeticLocationData", - "LinearChangingState", - "DictionaryState", - "DataclassState", - "CartesianLocationChangingState", - "State", - "GeodeticLocationChangingState", - "DetectabilityState", - "MultiStoreState", - "ResourceState", - "CommunicationStore", - "DecisionTask", - "Task", - "process", - "InterruptStates", - "TerminalTask", - "TaskNetwork", - "TaskNetworkFactory", - "TaskLinks", - "TaskNetworkNucleus", - "NucleusInterrupt", - "SharedLinearChangingState", - "PointToPointCommsManager", - "RoutingTableCommsManager", - "Message", - "MessageContent", - "SensorMotionManager", - "SteppedMotionManager", - "unit_convert", - "Routine", - "WindowedGet", -] diff --git a/src/upstage_des/base.py b/src/upstage_des/base.py deleted file mode 100644 index d952c44..0000000 --- a/src/upstage_des/base.py +++ /dev/null @@ -1,649 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Base classes and exceptions for UPSTAGE.""" - -from collections import defaultdict -from collections.abc import Generator, Iterable -from contextvars import ContextVar, Token -from dataclasses import dataclass, field -from math import floor -from random import Random -from time import gmtime, strftime -from typing import TYPE_CHECKING, Any, Protocol, Union -from warnings import warn - -from simpy import Environment as SimpyEnv -from simpy import Event as SimEvent - -from upstage_des.geography import INTERSECTION_LOCATION_CALLABLE, EarthProtocol -from upstage_des.units.convert import STANDARD_TIMES, TIME_ALTERNATES, unit_convert - -CONTEXT_ERROR_MSG = "Undefined context variable: use EnvironmentContext" - - -if TYPE_CHECKING: - from upstage_des.actor import Actor - from upstage_des.resources.monitoring import MonitoringMixin - - -SIMPY_GEN = Generator[SimEvent, Any, Any] - - -class DotDict(dict): - """A dictionary that supports dot notation as well as dictionary access notation. - - Usage: d = DotDict({'val1':'first'}) - set attributes: d.val2 = 'second' or d['val2'] = 'second' - get attributes: d.val2 or d['val2'] would both produce 'second' - """ - - def __getattr__(self, key: str) -> Any: - """Getattr with error for stage. - - Args: - key (str): The key - - Returns: - Any: The value - """ - if key not in self: - raise AttributeError(f"No key `{key}` found in stage. Use `UP.add_stage_variable`") - return self.get(key) - - def __setattr__(self, key: str, value: Any) -> None: - """Set the attribute. - - Typing is upset at a simple pass-through. - - Args: - key (str): Key - value (Any): Value - """ - if key in self: - raise AttributeError(f"Key {key} is already set.") - self.__setitem__(key, value) - - def __delattr__(self, key: str) -> None: - """Delete an attribute. - - Args: - key (str): Key - """ - del self[key] - - -class StageProtocol(Protocol): - """Protocol for typing the minimum entries in the Stage.""" - - @property - def altitude_units(self) -> str: - """Units of altitude.""" - - @property - def distance_units(self) -> str: - """Units of distance.""" - - @property - def stage_model(self) -> EarthProtocol: - """Model for geodetics.""" - - @property - def intersection_model(self) -> INTERSECTION_LOCATION_CALLABLE: - """Callable for geodetic intersections.""" - - @property - def time_unit(self) -> str: - """Time unit, Treated as 'hr' if not set. - - This value modifies ``pretty_now`` from ``UpstageBase``, - and can be used to modfy ``Wait`` timeouts. - """ - - @property - def random(self) -> Random: - """Random number generator.""" - - @property - def daily_time_count(self) -> float | int: - """The number of time_units in a "day". - - This value only modifies ``pretty_now`` from ``UpstageBase``. - - This is only used if the time_unit is not - s, min, or hr. In that case, 24 hour days are - assumed. - """ - - @property - def debug_log_time(self) -> bool: - """Whether or not times are logged as a string in the debug logs. - - Can be modified at the individual actor level with debug_log_time. - - Returns: - bool: If time is logged. - """ - - if TYPE_CHECKING: - - def __getattr__(self, key: str) -> Any: ... - - def __setattr__(self, key: str, value: Any) -> None: ... - - def __delattr__(self, key: str) -> None: ... - - -class UpstageError(Exception): - """Raised when an UPSTAGE error happens or expectation is not met.""" - - -class SimulationError(UpstageError): - """Raised when a simulation error occurs.""" - - def __init__(self, message: str, time: float | None = None): - """Create an informative simulation error. - - Args: - message (str): Error message - time (float | None, optional): Time of the error. Defaults to None. - """ - msg = "Error in the simulation: " - if msg in message: - msg = "" - msg += f" at time {time}: " if time is not None else "" - self.message = msg + message - super().__init__(self.message) - - -class MotionAndDetectionError(SimulationError): - """A simulation error raised during motion detection.""" - - -class RulesError(UpstageError): - """Raised by the user when a simulation rule is violated.""" - - -class MockEnvironment: - """A fake environment that holds the ``now`` property and all-caps attributes.""" - - def __init__(self, now: float): - """Create the mock environment. - - Args: - now (float): The time the environment is at. - """ - self.now = now - - @classmethod - def mock(cls, env: Union[SimpyEnv, "MockEnvironment"]) -> "MockEnvironment": - """Create a mock environment from another environment. - - Args: - env (SimpyEnv | MockedEnvironment): The simpy environments - - Returns: - MockEnvironment: The mocked environment (time only) - """ - mock_env = cls(now=env.now) - # copy over any attributes if they are all-caps - for k, v in env.__dict__.items(): - if k.upper() == k and not k.startswith("_"): - setattr(mock_env, k, v) - return mock_env - - @classmethod - def run(cls, until: float | int) -> None: - """Method stub for playing nice with rehearsal. - - Args: - until (float | int): Placeholder - """ - raise UpstageError("You tried to use `run` on a mock environment") - - -@dataclass -class SpecialContexts: - """Accessible lists of typed objects for contexts.""" - - actors: list["Actor"] = field(default_factory=list) - monitored: list["MonitoringMixin"] = field(default_factory=list) - data_recorded: list[tuple[float, Any]] = field(default_factory=list) - - -ENV_CONTEXT_VAR: ContextVar[SimpyEnv] = ContextVar("Environment") -SPECIAL_ENTITY_CONTEXT_VAR: ContextVar[SpecialContexts] = ContextVar("SpecialContexts") -ENTITY_CONTEXT_VAR: ContextVar[dict[str, list["NamedUpstageEntity"]]] = ContextVar("Entities") -STAGE_CONTEXT_VAR: ContextVar[DotDict] = ContextVar("Stage") - - -SKIP_GROUPS: list[str] = ["Actor", "Task", "Location", "CartesianLocation", "GeodeticLocation"] - - -class UpstageBase: - """A base mixin class for everyone. - - Provides access to all context variables created by `EnvironmentContext`. - - >>> with EnvironmentContext(initial_time=0.0) as env: - >>> data = UpstageBase() - >>> assert data.env is env - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Simple init to check if environment should be set.""" - try: - _ = self.env - except UpstageError: - warn(f"Environment not created at instantiation of {self}") - super().__init__(*args, **kwargs) - - @property - def env(self) -> SimpyEnv: - """Return the environment. - - Returns: - SimpyEnv: SimPy environment. - """ - try: - env: SimpyEnv = ENV_CONTEXT_VAR.get() - except LookupError: - raise UpstageError("No environment found or set.") - return env - - @property - def stage(self) -> StageProtocol: - """Return the stage context variable. - - Returns: - StageProtocol: The stage, as defined in context. - """ - try: - stage = STAGE_CONTEXT_VAR.get() - except LookupError: - raise UpstageError("No stage found or set.") - return stage - - def get_actors(self) -> list["Actor"]: - """Return all actors that the director knows. - - Returns: - list[NamedUpstageEntity]: List of actors in the simulation. - """ - ans: list[Actor] = [] - try: - ans = SPECIAL_ENTITY_CONTEXT_VAR.get().actors - except LookupError: - raise UpstageError(CONTEXT_ERROR_MSG) - return ans - - def get_entity_group(self, group_name: str) -> list["NamedUpstageEntity"]: - """Get a single entity group by name. - - Args: - group_name (str): The name of the entity group. - - Returns: - list[NamedUpstageEntity]: List of entities in the group. - """ - ans: list[NamedUpstageEntity] = [] - try: - grps: dict[str, list[NamedUpstageEntity]] = ENTITY_CONTEXT_VAR.get() - ans = grps.get(group_name, []) - except LookupError: - raise UpstageError(CONTEXT_ERROR_MSG) - return ans - - def get_monitored(self) -> list["MonitoringMixin"]: - """Return entities that inherit from the MonitoringMixin. - - Returns: - list[MonitoringMixin]: List of entitites that are monitoring. - """ - ans: list[MonitoringMixin] = [] - try: - ans = SPECIAL_ENTITY_CONTEXT_VAR.get().monitored - except LookupError: - raise UpstageError(CONTEXT_ERROR_MSG) - return ans - - def get_recorded(self) -> list[tuple[float, Any]]: - """Return custom recorded data. - - Returns: - list[tuple[float, Any]]: Lists of time and data object - """ - ans: list[tuple[float, Any]] = [] - try: - ans = SPECIAL_ENTITY_CONTEXT_VAR.get().data_recorded - except LookupError: - raise UpstageError(CONTEXT_ERROR_MSG) - return ans - - def get_all_entity_groups(self) -> dict[str, list["NamedUpstageEntity"]]: - """Get all entity groups. - - Returns: - dict[str, list[NamedUpstageEntity]]: Entity group names and associated - entities. - """ - grps: dict[str, list[NamedUpstageEntity]] = {} - try: - grps = ENTITY_CONTEXT_VAR.get() - except LookupError: - raise UpstageError(CONTEXT_ERROR_MSG) - return grps - - @property - def pretty_now(self) -> str: - """A well-formatted string of the sim time. - - Tries to account for generic names for time, such as 'ticks'. - - Returns: - str: The sim time - """ - now = self.env.now - time_unit = self.stage.get("time_unit", None) - # If it's explicitly set to None, still treat it as hours. - time_unit = "hr" if time_unit is None else time_unit - standard = TIME_ALTERNATES.get(time_unit.lower(), time_unit) - - ts: str - if standard in STANDARD_TIMES: - now_hrs = unit_convert(now, time_unit, "hr") - day = floor(now_hrs / 24) - rem_hours = now_hrs - (day * 24) - hms = strftime("%H:%M:%S", gmtime(rem_hours * 3600)) - ts = f"[Day {day:4.0f} - {hms:s}]" - else: - day_unit_count = self.stage.get("daily_time_count", None) - if day_unit_count is None: - ts = f"[{now:.3f} {time_unit}]" - else: - days = int(floor(now / day_unit_count)) - rem = now - (days * day_unit_count) - ts = f"[Day {days:4d} - {rem:.3f} {time_unit}]" - - return ts - - -class NamedUpstageEntity(UpstageBase): - """A base class for naming entities, and retrieving them. - - This creates a record of every instance of a subclass of this class. - - Example: - >>> class RocketCar(NamedUpstageEntity, entity_groups=["car", "fast"]) - >>> ... - >>> rc = RocketCar() - >>> assert rc in rc.get_entity_group("car") - """ - - _entity_groups: set[str] - - def _add_to_group(self, group_name: str) -> None: - """Add to a single group. - - Args: - group_name (str): Group name - """ - try: - ans = ENTITY_CONTEXT_VAR.get() - ans.setdefault(group_name, []) - if self in ans[group_name]: - raise UpstageError(f"Entity: {self} already recorded in the environment") - ans[group_name].append(self) - except LookupError: - entity_groups = {group_name: [self]} - ENTITY_CONTEXT_VAR.set(entity_groups) - - def _add_special_group(self) -> None: - """Add to a special group. - - Sub-classable for type help. - - Make sure that whatever entity group name this goes to is in SKIP_GROUPS. - """ - ... - - def _add_entity(self, group_names: set[str]) -> None: - """Add self to an entity group(s). - - Args: - group_names (list[str]): Group names to add to - """ - for group_name in group_names: - if group_name in SKIP_GROUPS: - continue - self._add_to_group(group_name) - self._add_special_group() - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Init the named entity.""" - super().__init__(*args, **kwargs) - self._add_entity(self._entity_groups) - - @classmethod - def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: - entity_groups: Iterable[str] | str | None = kwargs.get("entity_groups") - add_to_entity_groups: bool = kwargs.get("add_to_entity_groups", True) - skip_classname: bool = kwargs.get("skip_classname", False) - cls._entity_groups = set() - if not add_to_entity_groups: - return - - entity_groups = [] if entity_groups is None else entity_groups - - if isinstance(entity_groups, str): - entity_groups = [entity_groups] - - entity_groups = set(entity_groups) - - if cls.__name__ not in entity_groups and not skip_classname: - entity_groups.add(cls.__name__) - - for base in cls.mro(): - for grp in getattr(base, "_entity_groups", set()): - entity_groups.add(grp) - - cls._entity_groups = entity_groups - - -class SettableEnv(UpstageBase): - """A mixin class for allowing the instance's environment to change.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Passthrough for the mixed classes.""" - self._new_env: MockEnvironment | None = None - super().__init__(*args, **kwargs) - - @property # type: ignore [override] - def env(self) -> SimpyEnv | MockEnvironment: - """Get the relevant environment. - - Returns: - SimpyEnv | MockEnvironment: Real or mocked environment. - """ - if self._new_env is not None: - return self._new_env - return super().env - - @env.setter - def env(self, value: MockEnvironment) -> None: - if isinstance(value, MockEnvironment): - self._new_env = value - else: - # otherwise set new env back to none - self._new_env = None - - -class EnvironmentContext: - """A context manager to create a safe, globally (in context) referenceable environment and data. - - The environment created is of type simpy.Environment - - This also sets context variables for actors, entities, and the stage. - - Usage: - >>> with EnvironmentContext(initial_time=0.0) as env: - >>> env.run(until=3.0) - - This context manager is meant to be paired with inheritors of `UpstageBase`. - - that provides access to the context variables created in this manager. - - >>> class SimData(UpstageBase): - >>> ... - >>> - >>> with EnvironmentContext(initial_time=0.0) as env: - >>> data = SimData() - >>> assert data.env is env - - You may also provide a random seed, and a default Random() will be created with - that seed. - - >>> with EnvironmentContext(random_seed=1234986) as env: - >>> UpstageBase().stage.random.uniform(1, 3) - ... 2.348057489610457 - - Or your own RNG: - - >>> rng = Random(1234986) - >>> with EnvironmentContext(random_gen=rng) as env: - >>> UpstageBase().stage.random.uniform(1, 3) - ... 2.348057489610457 - """ - - def __init__( - self, - initial_time: float = 0.0, - random_seed: int | None = None, - random_gen: Any | None = None, - ) -> None: - """Create an environment context. - - random_seed is ignored if random_gen is given. Otherwise random.Random is - used. - - Args: - initial_time (float, optional): Time to start the clock at. Defaults to 0.0. - random_seed (int | None, optional): Seed for RNG. Defaults to None. - random_gen (Any | None, optional): RNG object. Defaults to None. - """ - self.env_ctx = ENV_CONTEXT_VAR - self.special_ctx = SPECIAL_ENTITY_CONTEXT_VAR - self.entity_ctx = ENTITY_CONTEXT_VAR - self.stage_ctx = STAGE_CONTEXT_VAR - self.env_token: Token[SimpyEnv] - self.special_token: Token[SpecialContexts] - self.entity_token: Token[dict[str, list[NamedUpstageEntity]]] - self.stage_token: Token[DotDict] - self._env: SimpyEnv | None = None - self._initial_time: float = initial_time - self._random_seed: int | None = random_seed - self._random_gen: Any = random_gen - - def __enter__(self) -> SimpyEnv: - """Create the environment context. - - Returns: - SimpyEnv: Simpy Environment - """ - self._env = SimpyEnv(initial_time=self._initial_time) - self.env_token = self.env_ctx.set(self._env) - self.special_token = self.special_ctx.set(SpecialContexts()) - self.entity_token = self.entity_ctx.set(defaultdict(list)) - stage = DotDict() - self.stage_token = self.stage_ctx.set(stage) - if self._random_gen is None: - random = Random(self._random_seed) - stage.random = random - else: - stage.random = self._random_gen - return self._env - - def __exit__(self, *_: Any) -> None: - """Leave the context.""" - self.env_ctx.reset(self.env_token) - self.special_ctx.reset(self.special_token) - self.entity_ctx.reset(self.entity_token) - self.stage_ctx.reset(self.stage_token) - self._env = None - - -def add_stage_variable(varname: str, value: Any) -> None: - """Add a variable to the stage. - - Will fail if it already exists. - - Args: - varname (str): Name of the variable - value (Any): Value to set it as - """ - try: - stage = STAGE_CONTEXT_VAR.get() - except LookupError: - raise ValueError("Stage should have been set.") - if varname in stage: - raise UpstageError(f"Variable '{varname}' already exists in the stage") - setattr(stage, varname, value) - - -def get_stage_variable(varname: str) -> Any: - """Get a variable from the context's stage. - - Args: - varname (str): Name of the variable - - Returns: - Any: The variable's value - """ - try: - stage = STAGE_CONTEXT_VAR.get() - except LookupError: - raise ValueError("Stage should have been set.") - if varname not in stage: - raise UpstageError(f"Variable '{varname}' does not exist in the stage") - return getattr(stage, varname) - - -def get_stage() -> StageProtocol: - """Return the entire stage object. - - Returns: - StageProtocol: The stage - """ - try: - stage = STAGE_CONTEXT_VAR.get() - except LookupError: - raise ValueError("Stage should have been set.") - return stage - - -def create_top_context( - initial_time: float = 0.0, - random_seed: int | None = None, - random_gen: Any | None = None, -) -> EnvironmentContext: - """Create a stage at this level of context. - - Makes your current level the same as the context manager. - - Returns: - EnvironmentContext: The context - """ - ctx = EnvironmentContext(initial_time, random_seed, random_gen) - ctx.__enter__() - return ctx - - -def clear_top_context(ctx: EnvironmentContext) -> None: - """Clear the context. - - Args: - ctx (EnvironmentContext): The object made from create_stage() - """ - ctx.__exit__() diff --git a/src/upstage_des/communications/__init__.py b/src/upstage_des/communications/__init__.py deleted file mode 100644 index cbc51a3..0000000 --- a/src/upstage_des/communications/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Module for communications processes and data objects.""" diff --git a/src/upstage_des/communications/comms.py b/src/upstage_des/communications/comms.py deleted file mode 100644 index 42370d9..0000000 --- a/src/upstage_des/communications/comms.py +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Comms message and commander classes.""" - -from collections.abc import Generator -from dataclasses import dataclass -from math import ceil -from typing import Any -from uuid import uuid4 - -from simpy import Event as SimpyEvent -from simpy import Store - -from upstage_des.actor import Actor -from upstage_des.base import ENV_CONTEXT_VAR, SimulationError, UpstageBase -from upstage_des.events import Put -from upstage_des.states import CommunicationStore -from upstage_des.task import process -from upstage_des.type_help import SIMPY_GEN - - -@dataclass -class MessageContent: - """Message content data object.""" - - data: dict - message: str | None = None - - -@dataclass -class Message: - """A message data object.""" - - sender: Actor - content: MessageContent - destination: Actor - - header: str | None = None - time_sent: float | None = None - time_received: float | None = None - - mode: str | None = None - - def __post_init__(self) -> None: - self.uid = uuid4() - self.time_created = ENV_CONTEXT_VAR.get().now - - def __hash__(self) -> int: - return hash(self.uid) - - -class CommsManagerBase(UpstageBase): - """A class to manage point to point transfer of communications. - - Works through simpy.Store or similar interfaces. Allows for degraded comms and comms retry. - - If an Actor contains a `CommunicationStore`, this object will detect that - and use it as a destination. In that case, you also do not need to connect - the actor to this object. - - Example: - >>> class Talker(UP.Actor): - >>> comms = UP.ResourceState[SIM.Store](default=SIM.Store) - >>> - >>> talker1 = Talker(name='MacReady') - >>> talker2 = Talker(name='Childs') - >>> - >>> comm_station = UP.CommsManager(name="Outpost 31", mode="voice") - >>> comm_station.connect(talker1, talker1.comms) - >>> comm_station.connect(talker2, talker2.comms) - >>> - >>> comm_station.run() - >>> - >>> # Typically, do this inside a task or somewhere else - >>> putter = comm_station.make_put( - >>> message="Grab your flamethrower!", - >>> source=talker1, - >>> destination=talker2, - >>> rehearsal_time_to_complete=0.0, - >>> ) - >>> yield putter - ... - >>> env.run() - >>> talker2.comms.items - [Message(sender=Talker: MacReady, message='Grab your flamethrower!', - destination=Talker: Childs)] - """ - - def __init__( - self, - *, - name: str, - mode: str | None = None, - init_entities: list[tuple[Actor, str]] | None = None, - send_time: float = 0.0, - retry_max_time: float = 1.0, - retry_rate: float = 0.166667, - debug_logging: bool = False, - ) -> None: - """Create a comms transfer manager. - - Parameters - ---------- - name : str - Give the instance a unique name for logging purposes - mode: str - The name of the mode comms are occurring over. Used for automated - detection of actor comms interfaces. - Default is None, which requires explicit connections. - init_entities : List[Tuple(instance, str)], optional - Entities who have a comms store to let the manager know about. The - tuples are (entity_instance, entity's comms input store's name), by default None - send_time : float, optional - Time to send a message, by default 0.0 - retry_max_time : float, optional - Amount of time (in sim units) to try resending a message, by default 1 - retry_rate : float, optional - How often (in sim units) to try re-sending a message, by default 10/60 - debug_logging : bool, optional - Turn on or off logging, by default False - """ - super().__init__() - self.name = name - self.mode = mode - self.comms_degraded: bool = False - self.retry_max_time = retry_max_time - self.retry_rate = retry_rate - self.send_time = send_time - self.incoming = Store(env=self.env) - self.connected: dict[Actor, str] = {} - self.blocked_links: list[tuple[Actor, Actor]] = [] - self.blocked_nodes: list[Actor] = [] - if init_entities is not None: - for entity, comms_store_name in init_entities: - self.connect(entity, comms_store_name) - self.debug_log: list[dict[str, Any]] = [] - self.debug_logging: bool = debug_logging - - @staticmethod - def clean_message(message: str | Message) -> MessageContent: - """Test to see if an object is a message. - - If it is, return the message contents only. Otherwise return the message. - - Args: - message (str | Message): The message to clean - - Returns: - MessageContent: The message as a message content object. - """ - if isinstance(message, Message): - return message.content - return MessageContent(data={"message": message}) - - def connect(self, entity: Actor, comms_store_name: str) -> None: - """Connect an actor and its comms store to this comms manager. - - Args: - entity (Actor): The actor that will send/receive. - comms_store_name (str): The store state name for receiving - """ - self.connected[entity] = comms_store_name - - def _get_state(self, actor: Actor) -> str | None: - """Get the comms store for the right mode.""" - for name, state in actor._state_defs.items(): - if not isinstance(state, CommunicationStore): - continue - mode_key = state._modename - modes: set[str] = actor.__dict__.get(mode_key, set()) - if self.mode in modes: - return name - return None - - def store_from_actor(self, actor: Actor) -> Store: - """Retrieve a communications store from an actor. - - Args: - actor (Actor): The actor - - Returns: - Store: A Comms store. - """ - if actor not in self.connected: - try: - msg_store_name = self._get_state(actor) - except SimulationError as e: - e.add_note(f"No comms destination on actor {actor}") - raise e - else: - msg_store_name = self.connected[actor] - - if msg_store_name is None: - raise SimulationError(f"No comms store on {actor}") - store: Store | None = getattr(actor, msg_store_name) - if store is None: - raise SimulationError(f"Bad comms store name: {msg_store_name} on {actor}") - return store - - def make_put( - self, - message: str | Message | MessageContent | dict, - source: Actor, - destination: Actor, - rehearsal_time_to_complete: float = 0.0, - ) -> Put: - """Create a Put request for a message into the CommsManager. - - Parameters - ---------- - source : - The message sender - destination : - The message receiver, who must be connected to the CommsManager - message : - Arbitrary data to send - rehearsal_time_to_complete : float, optional - Planning time to complete the event (see Put), by default 0.0 - - Returns: - ------- - Put - UPSTAGE Put event object to yield from a task - """ - use: Message - if isinstance(message, Message): - use = message - elif isinstance(message, MessageContent): - use = Message(sender=source, content=message, destination=destination, mode=self.mode) - else: - content = ( - MessageContent(data=message) - if isinstance(message, dict) - else MessageContent(data={}, message=message) - ) - use = Message(sender=source, content=content, destination=destination, mode=self.mode) - - return Put( - self.incoming, - use, - rehearsal_time_to_complete=rehearsal_time_to_complete, - ) - - @process - def _do_transmit( - self, message: Message, destination: Actor - ) -> Generator[SimpyEvent, None, None]: - # User implemented method for how to transmit a message - raise NotImplementedError() - - @process - def run(self) -> Generator[SimpyEvent, Any, None]: - """Run the communications message passing. - - Yields: - Generator[SimpyEvent, Any, None]: Simpy Process - """ - while True: - message = yield self.incoming.get() - dest = message.destination - self._do_transmit(message, dest) - - def _link_compare(self, a_test: Actor, b_test: Actor) -> bool: - if a_test in self.blocked_nodes or b_test in self.blocked_nodes: - return True - for a, b in self.blocked_links: - if a_test is a and b_test is b: - return True - return False - - def _test_if_link_is_blocked(self, source: Actor, destination: Actor) -> bool: - """Test if a link is blocked. - - Args: - source (Actor): Sender - destination (Actor): Destination - - Returns: - bool: If the link is blocked. - """ - if self._link_compare(source, destination): - return True - return False - - def _log_attempt( - self, message: Message, source: Actor, destination: Actor, status: str - ) -> None: - """Log an attempt to send a message.""" - if not self.debug_logging: - return - msg = { - "time": self.env.now, - "event": status, - "message": message, - "current": source, - "destination": destination, - } - self.debug_log.append(msg) - - def _attempt(self, message: Message, source: Actor, destination: Actor) -> SIMPY_GEN: - """Try to send a message. - - Implies some kind of acknowledgement system. - """ - n_tries = ceil(self.retry_max_time / self.retry_rate) - n_taken = 0 - while self.comms_degraded or self._test_if_link_is_blocked(source, destination): - if n_taken == n_tries: - self._log_attempt(message, source, destination, "Stopped trying to send") - return False - n_taken += 1 - self._log_attempt(message, source, destination, "Can't send, waiting") - yield self.env.timeout(self.retry_rate) - return True - - -class PointToPointCommsManager(CommsManagerBase): - """A class to manage point to point transfer of communications. - - Works through simpy.Store or similar interfaces. Allows for degraded comms and comms retry. - - If an Actor contains a `CommunicationStore`, this object will detect that - and use it as a destination. In that case, you also do not need to connect - the actor to this object. - - Example: - >>> class Talker(UP.Actor): - >>> comms = UP.ResourceState[SIM.Store](default=SIM.Store) - >>> - >>> talker1 = Talker(name='MacReady') - >>> talker2 = Talker(name='Childs') - >>> - >>> comm_station = UP.CommsManager(name="Outpost 31", mode="voice") - >>> comm_station.connect(talker1, talker1.comms) - >>> comm_station.connect(talker2, talker2.comms) - >>> - >>> comm_station.run() - >>> - >>> # Typically, do this inside a task or somewhere else - >>> putter = comm_station.make_put( - >>> message="Grab your flamethrower!", - >>> source=talker1, - >>> destination=talker2, - >>> rehearsal_time_to_complete=0.0, - >>> ) - >>> yield putter - ... - >>> env.run() - >>> talker2.comms.items - [Message(sender=Talker: MacReady, message='Grab your flamethrower!', - destination=Talker: Childs)] - """ - - @process - def _do_transmit( - self, message: Message, destination: Actor - ) -> Generator[SimpyEvent, None, None]: - can_send = yield from self._attempt(message, message.sender, destination) - if not can_send: - return - - if self.send_time > 0: - yield self.env.timeout(self.send_time) - - self._log_attempt(message, message.sender, destination, "Sent message") - - # update the send time - message.time_sent = self.env.now - store = self.store_from_actor(destination) - yield store.put(message) diff --git a/src/upstage_des/communications/processes.py b/src/upstage_des/communications/processes.py deleted file mode 100644 index 0db5125..0000000 --- a/src/upstage_des/communications/processes.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Communications process helper.""" - -from collections.abc import Callable, Generator -from typing import Any - -from simpy import Event, Process, Store - -from upstage_des.communications.comms import Message, MessageContent, PointToPointCommsManager -from upstage_des.task import process - - -def generate_comms_wait( - incoming_store: Store, - callback: Callable[[MessageContent], Any], -) -> Callable[[], Process]: - """Create a process function to transfer communications to a callback. - - This hides cleanup and other stability functions from the user. - - Parameters - ---------- - incoming_store : A simpy or upstage store - The store that is linked to a CommsManager instance. - callback : function - The function to call with a received message - - Returns: - ------- - function - An UPSTAGE process function that passes messages - """ - - @process - def comms_wait_proc() -> Generator[Event, str | Message, None]: - while True: - message = yield incoming_store.get() - message = PointToPointCommsManager.clean_message(message) - callback(message) - - return comms_wait_proc diff --git a/src/upstage_des/communications/routing.py b/src/upstage_des/communications/routing.py deleted file mode 100644 index 4ea0d91..0000000 --- a/src/upstage_des/communications/routing.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Comms routing from a routing table lookup.""" - -from collections import defaultdict, deque - -from upstage_des.actor import Actor -from upstage_des.base import SimulationError -from upstage_des.communications.comms import CommsManagerBase, Message -from upstage_des.task import process -from upstage_des.type_help import SIMPY_GEN - - -def _shortest_path(network: dict[str, set[str]], start: str, goal: str) -> list[str]: - """Dijkstra for network path.""" - queue = deque([(start, [start])]) - visited = {start} - - while queue: - node, path = queue.popleft() - for neighbor in network.get(node, set()): - if neighbor == goal: - return path + [neighbor] - if neighbor not in visited: - visited.add(neighbor) - queue.append((neighbor, path + [neighbor])) - return [] - - -class RoutingCommsManagerBase(CommsManagerBase): - """A comms manager that routes messages according to a network.""" - - def __init__( - self, - *, - name: str, - mode: str | None = None, - send_time: float = 0.0, - retry_max_time: float = 1.0, - retry_rate: float = 0.166667, - global_ignore: bool = False, - debug_logging: bool = False, - ) -> None: - """Create a comms transfer manager. - - Args: - name (str): Give the instance a unique name for logging purposes - mode (str): The name of the mode comms are occurring over. Used for automated - detection of actor comms interfaces. Default is None, which requires - explicit connections. - send_time (float, optional): Time to send a message, by default 0.0 - retry_max_time (float, optional): Amount of time (in sim units) to try - resending a message. Default is 1 - retry_rate (float, optional): How often (in sim units) to try re-sending - a message. Default is 10/60 - global_ignore (bool, optional): If a bad node is ignored forever. - Defautls to False. - debug_logging : bool, optional - Turn on or off logging, by default False - """ - super().__init__( - name=name, - mode=mode, - init_entities=None, - send_time=send_time, - retry_max_time=retry_max_time, - retry_rate=retry_rate, - debug_logging=debug_logging, - ) - self.global_ignore = global_ignore - - def connect(self, entity: Actor, comms_store_name: str) -> None: - """RoutingManagerBase doesn't allow this method.""" - raise SimulationError( - "RoutingCommsManager doesn't use connect(). Use connect_nodes() instead." - ) - - def select_hop( - self, source: Actor, dest: Actor, ignore_nodes: list[Actor] | None = None - ) -> Actor | None: - """Subclassable method for selecting the next hop in a route. - - Args: - source (Actor): Current point - dest (Actor): Message Destination - ignore_nodes (list[Actor], optional): Nodes to exclude. - - Returns: - Actor | None: Next hop to make. None if blocked path - """ - raise NotImplementedError("Implement this method.") - - @process - def _do_transmit(self, message: Message, destination: Actor) -> SIMPY_GEN: - # Take the message through the routing table - curr = message.sender - failed_nodes: list[Actor] = [] - while curr is not destination: - hop = self.select_hop(curr, destination, failed_nodes) - if hop is None: - self._log_attempt(message, curr, destination, "No message route available") - return - can_send = yield from self._attempt(message, curr, hop) - # replan on fail - if not can_send: - failed_nodes.append(hop) - # take this hop and see if we can send it - continue - # we can send it - self._log_attempt(message, curr, hop, "Moved message") - if self.send_time > 0: - yield self.env.timeout(self.send_time) - # Allow all nodes again - if not self.global_ignore: - failed_nodes = [] - # Update position in the network - curr = hop - # we've reached the destination - self._log_attempt(message, curr, destination, "Destination reached") - message.time_sent = self.env.now - store = self.store_from_actor(destination) - yield store.put(message) - - -class RoutingTableCommsManager(RoutingCommsManagerBase): - """Route comms according to a pre-defined network. - - Nodes (Actors) must be explicitly connected, and this manager will - route through shortest number of hops. - - Allows for degraded comms and comms retry. If a link is not degraded, - after the retry fails the network will re-plan a route assuming the intermediate - destination node is no longer available. - - The behavior is: - - 1. Ask for transmit from SOURCE to DEST - 2. Set CURRENT to SOURCE - 3. Find the NEXT in the shortest path from CURRENT to DEST - 4. If there is no path, stop trying to send and end. - 5. Attempt to send to NEXT (this is the degraded comms/retry step) - 6. If it can send, do so. Set CURRENT = NEXT. If NEXT is DEST, Goto 8. Otherwise, Goto 3. - 7. If it can't send, drop NEXT from the route options. Goto 3 - 8. Place message in DEST and end. - - Since this is time-based, a link can re-open during transmission. If the - network has paths: - - A -> B -> C - A -> D -> E -> F -> G -> H -> C - E -> B -> C - - and we want to send from A to C, but B is blocked, a retry will have the - network attempt to take the long way through DEFGHC. If B comes back online - after the message gets to E, the routing will choose EBC instead. - - If B does not come back online, the router will still try to go to B from E - since that is shorter. If B is still down, it will take longer due to the - retry. Set the input ``global_ignore`` to ``True`` to ignore a bad node - for the entire routing and avoid this behavior. - - Example: - - .. code-block:: python - - class CommNode(Actor): - messages = CommunicationStore(modes=None) - - with EnvironmentContext() as env: - nodes = { - name: CommNode(name=name, messages={"modes":["cup-and-string"]}) - for name in "ABCDEFGH" - } - mgr = RoutingTableCommsManager( - name="StaticManager", - mode="cup-and-string", - send_time=1/3600., - retry_max_time=20/3600., - retry_rate=4/3600., - ) - for u, v in ["AB", "BC", "AD", "DE", "EF", "FG", "GH", "HC", "EB"]: - mgr.connect_nodes(nodes[u], nodes[v]) - - """ - - def __init__( - self, - *, - name: str, - mode: str | None = None, - send_time: float = 0.0, - retry_max_time: float = 1.0, - retry_rate: float = 0.166667, - global_ignore: bool = False, - debug_logging: bool = False, - ) -> None: - """Create a static network structure for message routing. - - Args: - name (str): Give the instance a unique name for logging purposes - mode (str): The name of the mode comms are occurring over. Used for automated - detection of actor comms interfaces. Default is None, which requires - explicit connections. - send_time (float, optional): Time to send a message, by default 0.0 - retry_max_time (float, optional): Amount of time (in sim units) to try - resending a message. Default is 1 - retry_rate (float, optional): How often (in sim units) to try re-sending - a message. Default is 10/60 - global_ignore (bool, optional): If a bad node is ignored forever. - Defautls to False. - debug_logging : bool, optional - Turn on or off logging, by default False - """ - super().__init__( - name=name, - mode=mode, - send_time=send_time, - retry_max_time=retry_max_time, - retry_rate=retry_rate, - global_ignore=global_ignore, - debug_logging=debug_logging, - ) - self._nodes: dict[str, Actor] = {} - self._network: dict[str, set[str]] = defaultdict(set) - - def connect_nodes(self, u: Actor, v: Actor, two_way: bool = False) -> None: - """Connect node u to v (one-way). - - Make the connection two-way with the last argument. - - Args: - u (Actor): The source actor - v (Actor): Destination actor - two_way (bool, optional): If the connection is two way. Defaults to False. - """ - # test that the nodes have a store for comms on the mode of this manager - for act in [u, v]: - if self._get_state(act) is None: - raise SimulationError(f"Actor {act} has no comms store on mode {self.mode}") - - self._nodes[u.name] = u - self._nodes[v.name] = v - self._network[u.name].add(v.name) - if two_way: - self._network[v.name].add(u.name) - - def select_hop( - self, source: Actor, dest: Actor, ignore_nodes: list[Actor] | None = None - ) -> Actor | None: - """Method for selecting the next hop to make. - - This selects the shortest number of hops. - - Args: - source (Actor): Starting or current point - dest (Actor): Destination - ignore_nodes (list[Actor], optional): Nodes to exclude. - - Returns: - Actor | None: The next place to go or None if no route. - """ - u, v = source.name, dest.name - if u not in self._network: - return None - if v in self._network[u]: - return dest - ignore_names = [x.name for x in ignore_nodes] if ignore_nodes is not None else [] - net = {k: v - set(ignore_names) for k, v in self._network.items() if k not in ignore_names} - path = _shortest_path(net, u, v) - if not path: - return None - # return the next step - return self._nodes[path[1]] diff --git a/src/upstage_des/constants.py b/src/upstage_des/constants.py deleted file mode 100644 index 376c0e5..0000000 --- a/src/upstage_des/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""The constant values used by UPSTAGE.""" - -__all__ = ("PLANNING_FACTOR_OBJECT",) - - -PLANNING_FACTOR_OBJECT = object() diff --git a/src/upstage_des/data_types.py b/src/upstage_des/data_types.py deleted file mode 100644 index 84adeb8..0000000 --- a/src/upstage_des/data_types.py +++ /dev/null @@ -1,632 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Data types for common operations. Currently just locations.""" - -from dataclasses import FrozenInstanceError -from math import degrees, radians, sqrt -from typing import Any - -from upstage_des.base import UpstageBase -from upstage_des.math_utils import _vector_norm, _vector_subtract -from upstage_des.units import unit_convert - -__all__ = ( - "CartesianLocation", - "GeodeticLocation", - "Location", - "CartesianLocationData", - "GeodeticLocationData", -) - - -class Location(UpstageBase): - """An abstract class for representing a location in a space.""" - - def copy(self) -> "Location": - """Copy the location.""" - raise NotImplementedError("Subclass must implement copy.") - - @property - def _repred_attrs(self) -> dict[str, Any]: - """A way to customize how attributes are represented by __repr__. - - Returns: - dict[str, Any]: Relevant attributes with name:value pairs. - """ - return { - k: v - for k, v in self.__dict__.items() - if not k.startswith("_") and k not in ["env", "stage"] - } - - def _key(self) -> tuple[float, ...]: - """A key used for hashing.""" - raise NotImplementedError("Location is intended to be subclassed.") - - def _to_tuple(self) -> tuple[float, ...]: - """Return a tuple of the location. - - To be implemented in a subclass. - - Returns: - tuple[float, ...]: Tuple of numbers describing a location. - """ - raise NotImplementedError("Subclass must implement tuple.") - - def straight_line_distance(self, other: object) -> float: - """Straight line distances b/w locations. - - Args: - other (Location): Another location - """ - raise NotImplementedError( - "Subclass must implement a subtraction operator to calculate distance between Locations" - ) - - def __setattr__(self, name: str, value: Any) -> None: - """Locations should be frozen, so this setattr restricts changing the values. - - Args: - name (str): Attribute name - value (Any): Attribute value - """ - if hasattr(self, "_no_override") and name in self._no_override: - raise FrozenInstanceError(f"Locations are disallowed from setting {name}") - return super().__setattr__(name, value) - - def __sub__(self, other: object) -> float: - """Subtract location. - - Args: - other (Location): Another location - """ - raise NotImplementedError( - "Subclass must implement a subtraction operator to calculate distance between Locations" - ) - - def __eq__(self, other: object) -> bool: - """Test for equality with another location. - - Args: - other (object): The other location object - - Returns: - bool: If it is equal. - """ - raise NotImplementedError("Subclass must implement a equality comparison") - - def __hash__(self) -> int: - raise NotImplementedError("Location is not intended for solo use.") - - def __repr__(self) -> str: - """Customized the printable representation of the object. - - Does this by: - 1. Using all the dataclass fields that have `repr` set to True - 2. Representation for attributes can be customized by defining a - `_get_repred_attrs` method that returns a dictionary of attributes - keys and repr'ed values. - """ - clean_attrs = [f"{key}={value}" for key, value in self._repred_attrs.items()] - return f"{self.__class__.__name__}({', '.join(clean_attrs)})" - - -class CartesianLocation(Location): - """A location that can be mapped to a 3-dimensional cartesian space.""" - - def __init__( - self, - x: float, - y: float, - z: float = 0.0, - *, - use_altitude_units: bool = False, - ) -> None: - """A Cartesian (3D space) location. - - use_altitude_units, when false, means Z distance uses the "distance_units" unit. - - When true, it uses "altitude_units". - - Use UP.add_stage_variable("altitude_units", "ft"), e.g. - - Args: - x (float): X dimension location - y (float): Y dimension location - z (float, optional): Z location. Defaults to 0.0. - use_altitude_units (bool, optional): Use the sim's altitude units. Defaults to False. - """ - super().__init__() - self.x = x - self.y = y - self.z = z - self.use_altitude_units = use_altitude_units - self._no_override = ["x", "y", "z", "use_altitude_units"] - - @property - def _repred_attrs(self) -> dict[str, str]: - """Allows for attributes to be repr'ed based on the state of the units. - - Returns: - dict[str, str]: Strings of attributes. - """ - attrs = {} - try: - hor_units = self.stage.distance_units - except AttributeError: - hor_units = "" - try: - alt_units = ( - self.stage.altitude_units if self.use_altitude_units else self.stage.distance_units - ) - except AttributeError: - alt_units = "" - for coordinate in ("x", "y"): - attrs[coordinate] = f"{getattr(self, coordinate):,}{hor_units}" - attrs["z"] = f"{self.z:,}{alt_units}" - return attrs - - def _as_array(self) -> tuple[float, float, float]: - """Make an array of consistent units for all dimensions. - - Returns: - tuple[float, float, float]: A 1-D array of (x, y, z) - """ - if self.use_altitude_units: - height = unit_convert(self.z, self.stage.altitude_units, self.stage.distance_units) - else: - height = self.z - return (self.x, self.y, height) - - def _to_tuple(self) -> tuple[float, float, float]: - """Return a tuple of the location. - - Returns: - tuple[float, float, float]: Latitude, longitude, altitude. - """ - return self._as_array() - - def copy(self) -> "CartesianLocation": - """Return a copy of the location. - - Returns: - CartesianLocation - """ - return self.__class__( - x=self.x, y=self.y, z=self.z, use_altitude_units=self.use_altitude_units - ) - - def _key(self) -> tuple[float, float, float, bool]: - """Key for hashing. - - Returns: - tuple[float, float, float, bool] - """ - return (self.x, self.y, self.z, self.use_altitude_units) - - def straight_line_distance(self, other: object) -> float: - """Get the straight line distance between this and another location. - - Args: - other (object): The other CartesianLocation point. - """ - return self - other - - def __getitem__(self, idx: int) -> float: - """Convenience way to get an xyz position by index. - - Args: - idx (int): Index (xyz) - - Returns: - float: Value at index - """ - if not 0 <= idx <= 2: - raise ValueError(f"CartesianLocation only has 3 indices (x, y, z), not a {idx}th index") - return [self.x, self.y, self.z][idx] - - def __sub__(self, other: object) -> float: - """Subtract one cartesian location from another. - - Distance is straight lint. - - Args: - other (CartesianLocation): Another location - - Returns: - float: Distance between this and another location. - """ - if isinstance(other, CartesianLocation): - sum_sq = sum((a - b) ** 2 for a, b in zip(self._as_array(), other._as_array())) - return sqrt(sum_sq) - else: - raise ValueError(f"Cannot subtract {other.__class__.__name__} from a CartesianLocation") - - def __eq__(self, other: object) -> bool: - """Test if two positions are the same. - - Uses a tolerance so this will be True for very close positions. - - Args: - other (CartesianLocation): Another location - - Returns: - bool: Is equal or not - """ - if not isinstance(other, CartesianLocation): - raise ValueError(f"Cannot compare {other.__class__.__name__} to a CartesianLocation") - dist = self - other - return bool(abs(dist) <= 0.00001) - - def __hash__(self) -> int: - """Hash based on the key. - - Returns: - int: The hash. - """ - return hash(self._key()) - - -class GeodeticLocation(Location): - """A Location that can be mapped to the surface of a spheroid (a.k.a. ellipsoid). - - More specifically, a Location representing somewhere on an ellipsoid, with Latitude, - Longitude, and Altitude that uses the geodetic datum (or geodetic system). Can be - used to define a location on Earth or other planetary bodies. - - Units for the horizontal datum (i.e., `lat` and `lon`) can be Decimal Degrees or - Radians, depending on the value of `units`, the vertical datum (i.e., `alt`) is - assumed to be in meters. - - Subtraction represents a great circle distance, NOT a true 3D straight-line distance. - - Speeds used for this location type will represent ground speed, which allows - the class to ignore solving the exact altitude change in the path. - - Units are an input to the location type. - - The ellipsoid model must have a `.distance(Location1, Location2)` method - that looks for .lat and .lon. - - `altitude` is 0.0 by default. - - `lat` (latitude) and `lon` (longitude) are in degrees by default. - if using radians, set `in_radians` to `True`. - - """ - - def __init__( - self, - lat: float, - lon: float, - alt: float = 0.0, - *, - in_radians: bool = False, - ) -> None: - """A location on a geodetic (Earth). - - Altitude uses the "altitude_units" stage variable. - - Args: - lat (float): Latitude (North/South) - lon (float): Longitude (East/West) - alt (float, optional): Altitude. Defaults to 0.0. - in_radians (bool, optional): If the lat/lon are in radians or degrees. - Defaults to False. - - Returns: - GeodeticLocation - """ - super().__init__() - self.lat = lat - self.lon = lon - self.alt = alt - self.in_radians = in_radians - self._no_override = ["lat", "lon", "alt", "in_radians"] - - @property - def _repred_attrs(self) -> dict[str, str]: - """Allows for attributes to be repr'ed based on the state of the units. - - Returns: - dict[str, str]: Strings of attributes. - """ - attrs = {} - units = "rad" if self.in_radians else "°" - try: - alt_units = self.stage.altitude_units - except AttributeError: - alt_units = "" - for coordinate in ("lat", "lon"): - attrs[coordinate] = f"{getattr(self, coordinate)}{units}" - attrs["alt"] = f"{self.alt:,}{alt_units}" - return attrs - - def _to_tuple(self) -> tuple[float, float, float]: - """Return a tuple of the location. - - Returns: - tuple[float, float, float]: Latitude, longitude, altitude. - """ - return (self.lat, self.lon, self.alt) - - def latlon(self) -> tuple[float, float]: - """Return a tuple of just lat/lon as degrees. - - Returns: - tuple[float, float]: Latitude and longitude in degrees. - """ - s = self.to_degrees() - return (s.lat, s.lon) - - def to_radians(self) -> "GeodeticLocation": - """Convert to radians, if already in radians, return self. - - Returns: - GeodeticLocation - """ - if self.in_radians: - return self - kwargs: dict[str, float | bool] = {"alt": self.alt} - for coordinate in ("lat", "lon"): - kwargs[coordinate] = radians(getattr(self, coordinate)) - kwargs["in_radians"] = True - return self.__class__(**kwargs) # type: ignore [arg-type] - - def to_degrees(self) -> "GeodeticLocation": - """Convert to degrees, if already in degrees, return self. - - Returns: - GeodeticLocation - """ - if not self.in_radians: - return self - kwargs: dict[str, float | bool] = {"alt": self.alt} - for coordinate in ("lat", "lon"): - kwargs[coordinate] = degrees(getattr(self, coordinate)) - kwargs["in_radians"] = False - return self.__class__(**kwargs) # type: ignore [arg-type] - - def _key(self) -> tuple[float, float, float, bool]: - """A key for hashing. - - Returns: - tuple[float, float, float, bool]: All values. - """ - return (self.lat, self.lon, self.alt, self.in_radians) - - def copy(self) -> "GeodeticLocation": - """Copy the location. - - Returns: - GeodeticLocation - """ - return self.__class__( - lat=self.lat, - lon=self.lon, - alt=self.alt, - in_radians=self.in_radians, - ) - - def dist_with_altitude(self, other: "GeodeticLocation") -> float: - """Get the distance between two points with an altitude component. - - Args: - other (GeodeticLocation): The other point - - Returns: - float: Distance - pythagorean of great-circle and altitude - """ - dist = self - other - alt = abs(self.alt - other.alt) - alt = unit_convert(alt, self.stage.altitude_units, self.stage.distance_units) - full_dist: float = sqrt(alt**2 + dist**2) - return full_dist - - def straight_line_distance(self, other: object) -> float: - """Straight-line distance, using ECEF. - - This won't account for horizon. - - Args: - other (GeodeticLocation): The other point - - Returns: - float: Distance - """ - if not isinstance(other, GeodeticLocation): - raise TypeError(f"Cannot subtract a {other.__class__.__name__} from a GeodeticLocation") - lat, lon, alt = self.to_degrees()._to_tuple() - alt = unit_convert(alt, self.stage.altitude_units, "m") - ecef_self = self.stage.stage_model.lla2ecef([(lat, lon, alt)])[0] - - lat, lon, alt = other.to_degrees()._to_tuple() - alt = unit_convert(alt, self.stage.altitude_units, "m") - ecef_other = self.stage.stage_model.lla2ecef([(lat, lon, alt)])[0] - - dist_meters = float(_vector_norm(_vector_subtract(ecef_other, ecef_self))) - dist_units = unit_convert(dist_meters, "m", self.stage.distance_units) - return dist_units - - def __getitem__(self, idx: int) -> float: - """Convenience way to get an xyz position by index. - - Args: - idx (int): Index (lat/lon/alt) - - Returns: - float: Value at index - """ - if not 0 <= idx <= 2: - raise IndexError( - f"GeodeticLocation only has 3 indices (lat, lon, alt), not a {idx}th index" - ) - return [self.lat, self.lon, self.alt][idx] - - def __sub__(self, other: object) -> float: - """Find the great circle distance between Geodetic points. - - Args: - other (GeodeticLocation): Another location - - Returns: - float: distance in stage "distance_units" units. - """ - if not isinstance(other, GeodeticLocation): - raise ValueError( - f"Cannot subtract a {other.__class__.__name__} from a GeodeticLocation" - ) - # distances presume positions are in degrees - dlat, dlon = self.to_degrees()._key()[:2] - olat, olon = other.to_degrees()._key()[:2] - dist = self.stage.stage_model.distance( - (dlat, dlon), - (olat, olon), - units=self.stage.distance_units, - ) - return dist - - def __eq__(self, other: object) -> bool: - """Test if two locations are the same. - - No tolerance is applied here. - - Args: - other (GeodeticLocation): Another location - - Returns: - bool: Close enough or not. - """ - if not isinstance(other, GeodeticLocation): - raise ValueError(f"Cannot compare a {other.__class__.__name__} to a GeodeticLocation") - if other.in_radians != self.in_radians: - if self.in_radians: - other = other.to_radians() - else: - other = other.to_degrees() - return all( - getattr(self, dimension) == getattr(other, dimension) - for dimension in ["lat", "lon", "alt"] - ) - - def __hash__(self) -> int: - """Hash based on the key. - - Returns: - int: The hash. - """ - return hash(self._key()) - - -class CartesianLocationData: - """Object for storing caretesian data without an environment.""" - - def __init__( - self, - x: float, - y: float, - z: float = 0.0, - *, - use_altitude_units: bool = True, - ) -> None: - """Cartesian data storage. - - Args: - x (float): _description_ - y (float): _description_ - z (float, optional): _description_. Defaults to 0.0. - use_altitude_units (bool, optional): _description_. Defaults to True. - """ - self.x = x - self.y = y - self.z = z - self.use_altitude_unts = use_altitude_units - - def make_location(self) -> CartesianLocation: - """Create a location from the data. - - Do this inside an environment contex.t - - Returns: - CartesianLocation: The location object. - """ - return CartesianLocation( - x=self.x, y=self.y, z=self.z, use_altitude_units=self.use_altitude_unts - ) - - def __eq__(self, value: Any) -> bool: - """Test for equality of two cartesian locations data objects.""" - if not isinstance(value, CartesianLocationData): - raise ValueError( - f"Cannot compare a {value.__class__.__name__} to a CartesianLocationData" - ) - check = ["x", "y", "z"] - return all(getattr(self, c) == getattr(value, c) for c in check) - - -class GeodeticLocationData: - """Object for storing geodetic data without an environment.""" - - def __init__( - self, - lat: float, - lon: float, - alt: float = 0.0, - *, - in_radians: bool = False, - ) -> None: - """Geodetic data storage. - - Args: - lat (float): _description_ - lon (float): _description_ - alt (float, optional): _description_. Defaults to 0.0. - in_radians (bool, optional): _description_. Defaults to False. - """ - self.lat = lat - self.lon = lon - self.alt = alt - self.in_radians = in_radians - - def latlon(self) -> tuple[float, float]: - """Return a tuple of just lat/lon as degrees. - - Returns: - tuple[float, float]: Latitude and longitude in degrees. - """ - lat = degrees(self.lat) if self.in_radians else self.lat - lon = degrees(self.lon) if self.in_radians else self.lon - return (lat, lon) - - def make_location(self) -> GeodeticLocation: - """Create a location from the data. - - Do this inside an environment contex.t - - Returns: - GeodeticLocation: The location object. - """ - return GeodeticLocation( - lat=self.lat, - lon=self.lon, - alt=self.alt, - in_radians=self.in_radians, - ) - - def __eq__(self, value: Any) -> bool: - """Test for equality of two cartesian locations data objects.""" - if not isinstance(value, GeodeticLocationData): - raise ValueError( - f"Cannot compare a {value.__class__.__name__} to a GeodeticLocationData" - ) - other = [value.lat, value.lon] - if value.in_radians != self.in_radians: - if self.in_radians: - other = list(map(radians, other)) - else: - other = list(map(degrees, other)) - angles = [self.lat, self.lon] == other - return angles and self.alt == value.alt diff --git a/src/upstage_des/data_utils/__init__.py b/src/upstage_des/data_utils/__init__.py deleted file mode 100644 index c1dccc7..0000000 --- a/src/upstage_des/data_utils/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Utilities for data processing.""" - -from .data_recorder import DataRecorder, get_recorded_data, record_data -from .data_utils import create_location_table, create_table - -__all__ = [ - "create_table", - "create_location_table", - "DataRecorder", - "record_data", - "get_recorded_data", -] diff --git a/src/upstage_des/data_utils/data_recorder.py b/src/upstage_des/data_utils/data_recorder.py deleted file mode 100644 index adf6c51..0000000 --- a/src/upstage_des/data_utils/data_recorder.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Class for custom recording of things.""" - -from copy import deepcopy -from typing import Any - -from upstage_des.base import SPECIAL_ENTITY_CONTEXT_VAR, UpstageBase - - -class DataRecorder(UpstageBase): - """An UpstageBase subclass to help with data recording.""" - - def record_data(self, data: Any, copy: bool = False) -> None: - """Record any data at this time for retrieval later. - - The data recorded should be immutable or unchanging to preserve - the values of the data at a given time. E.g. don't supply an Actor - or other objects that change. Dictionaries or dataclasses are fine - as long as they are not used anywhere else. - - A deepcopy parameter is supplied to get around this, but it may not - work with all entities or objects. - - Args: - data (Any): Any object or data to record. - copy (bool, optional): Whether to attempt a deepcopy. - Defaults to False. - """ - ans = SPECIAL_ENTITY_CONTEXT_VAR.get().data_recorded - inp = data if not copy else deepcopy(data) - ans.append((self.env.now, inp)) - - -def record_data(data: Any, copy: bool = False) -> None: - """Record any data at this time for retrieval later. - - The data recorded should be immutable or unchanging to preserve - the values of the data at a given time. E.g. don't supply an Actor - or other objects that change. Dictionaries or dataclasses are fine - as long as they are not used anywhere else. - - A deepcopy parameter is supplied to get around this, but it may not - work with all entities or objects. - - This function must be run inside an environment context. - - Args: - data (Any): Any object or data to record. - copy (bool, optional): Whether to attempt a deepcopy. - Defaults to False. - """ - dr = DataRecorder() - dr.record_data(data, copy=copy) - - -def get_recorded_data() -> list[tuple[float, Any]]: - """Return all data recorded with record_data or DataRecorder. - - Returns: - list[tuple[float, Any]]: The data. - """ - dr = DataRecorder() - return dr.get_recorded() diff --git a/src/upstage_des/data_utils/data_utils.py b/src/upstage_des/data_utils/data_utils.py deleted file mode 100644 index b4eef68..0000000 --- a/src/upstage_des/data_utils/data_utils.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Utilities for gathering all recorded simulation data.""" - -from dataclasses import asdict, fields, is_dataclass -from typing import Any, cast - -from upstage_des.actor import Actor -from upstage_des.base import UpstageBase -from upstage_des.data_types import CartesianLocation, GeodeticLocation -from upstage_des.states import ( - ActiveState, - ActiveStatus, - CartesianLocationChangingState, - GeodeticLocationChangingState, - _DictionaryProxy, -) - -ACTUAL_LOCATION = GeodeticLocation | CartesianLocation -LOCATION_TYPES = ACTUAL_LOCATION | GeodeticLocationChangingState | CartesianLocationChangingState -STATIC_STATE = "Last Seen" - -STATE_DATA_ROW = tuple[str, str, str, float, Any, str | None] -LOCATION_DATA_ROW = tuple[str, str, str, float, float, float, float, str | None] -COLUMN_NAMES = ["Entity Name", "Entity Type", "State Name", "Time"] -ACTIVATION_STATUS_COL = "Activation Status" - - -def _state_history_to_table( - actor_name: str, - actor_kind: str, - state_name: str, - is_active: bool, - hist: list[tuple[float, Any]], -) -> list[STATE_DATA_ROW]: - """Create a state history table from an actor. - - The final entry is a way to flag if a variable is becoming active or not. - - Args: - actor_name (str): Actor name - actor_kind (str): Actor kind - state_name (str): State name - is_active (bool): If the state is an active type - hist (list[tuple[float, Any]]): History from _quantities or _state_histories - - Returns: - list[STATE_DATA_ROW]: A long-form data table of state data. - """ - data: list[STATE_DATA_ROW] = [] - active_value = "inactive" if is_active else None - for time, value in hist: - if isinstance(value, ActiveStatus): - row = data.pop(-1) - rows = [tuple(list(row[:-1]) + [value.name])] - active_value = "active" if value.name == "activating" else "inactive" - elif is_dataclass(value) and not isinstance(value, type): - rows = [ - (actor_name, actor_kind, f"{state_name}.{k}", time, v, active_value) - for k, v in asdict(value).items() - ] - elif isinstance(value, dict): - rows = [ - (actor_name, actor_kind, f"{state_name}.{k}", time, v, active_value) - for k, v in value.items() - ] - else: - rows = [(actor_name, actor_kind, state_name, time, value, active_value)] - data.extend(rows) - return data - - -def _key_list(obj: Any) -> list[str]: - if isinstance(obj, dict): - return [str(x) for x in obj.keys()] - if is_dataclass(obj): - return [f.name for f in fields(obj)] - raise ValueError(f"Unexpected data type for state history: {obj}") - - -def _actor_state_data( - actor: Actor, - skip_locations: bool = True, - save_static: bool = False, -) -> tuple[list[STATE_DATA_ROW], list[Any]]: - """Gather actor recorded data. - - Args: - actor (Actor): The actor. - skip_locations (bool, optional): If location states should be ignored. - Defaults to True. - save_static (bool, optional): If non-recording states are saved. - Defaults to False. - - Returns: - list[STATE_INFO]: List of state information - list[Any]: List of monitoring objects to ignore in a global search. - """ - data: list[STATE_DATA_ROW] = [] - resources: list[Any] = [] - name, kind = actor.name, actor.__class__.__name__ - - for state_name, state in actor._state_defs.items(): - if skip_locations and isinstance(state, LOCATION_TYPES): - continue - _value = actor.__dict__[state_name] - is_active = isinstance(state, ActiveState) - is_prefilled = any(key.startswith(f"{state_name}.") for key in actor._state_histories) - if state_name in actor._state_histories: - data.extend( - _state_history_to_table( - name, kind, state_name, is_active, actor._state_histories[state_name] - ) - ) - elif is_prefilled: - for key in _key_list(_value): - sname = f"{state_name}.{key}" - assert sname in actor._state_histories - data.extend( - _state_history_to_table( - name, kind, sname, is_active, actor._state_histories[sname] - ) - ) - elif hasattr(_value, "_quantities"): - resources.append(_value) - data.extend(_state_history_to_table(name, kind, state_name, False, _value._quantities)) - elif save_static: - the_value = getattr(actor, state_name) - if isinstance(the_value, _DictionaryProxy): - data.extend( - [ - (name, kind, f"{state_name}.{k}", 0.0, v, STATIC_STATE) - for k, v in _value.items() - ] - ) - else: - data.append((name, kind, state_name, 0.0, the_value, STATIC_STATE)) - - return data, resources - - -def _actor_location_data(actor: Actor) -> tuple[list[LOCATION_DATA_ROW], list[str]]: - """Get actor location data, if it exists. - - The actor needs to have recording Location states: - * CartesianLocation(ChangingState) - * GeodeticLocation(ChangingState) - - Args: - actor (Actor): The actor. - - Returns: - list[LOCATION_DATA_ROW]: Time and XYZ/LLA data. - list[str]: name of XYZ/LLA - """ - data: list[LOCATION_DATA_ROW] = [] - is_xyz = True - name, kind = actor.name, actor.__class__.__name__ - for state_name, state_data in actor._state_histories.items(): - _state = actor._state_defs.get(state_name, None) - if not isinstance(_state, LOCATION_TYPES): - continue - value: ACTUAL_LOCATION | ActiveStatus - is_active = isinstance(_state, ActiveState) - active_value = "inactive" if is_active else None - for time, value in state_data: - if isinstance(value, ActiveStatus): - _row = data.pop(-1) - row = tuple(list(_row[:-1]) + [value.name]) - active_value = "active" if value.name == "activating" else "inactive" - elif isinstance(value, GeodeticLocation): - row = (name, kind, state_name, time, value.lat, value.lon, value.alt, active_value) - is_xyz = False - elif isinstance(value, CartesianLocation): - row = (name, kind, state_name, time, value.x, value.y, value.z, active_value) - data.append(cast(LOCATION_DATA_ROW, row)) - cols = ["X", "Y", "Z"] if is_xyz else ["Lat", "Lon", "Alt"] - return data, cols - - -def create_table( - skip_locations: bool = True, save_static: bool = False -) -> tuple[list[STATE_DATA_ROW], list[str]]: - """Create a data table of everything UPSTAGE has recorded. - - This uses the current environment context. - - The data columns are: - Time, Entity Name, Entity Type, State Name, State Value - - For SelfMonitoring<> resources that are not part of an actor, the name - is pulled from the name entry to the resource. The Entity Type is the - class name, and the State Name is "Resource State". - - Usage: - - >>> import pandas as pd - >>> with UP.EnvironmentContext() as env: - >>> ... - >>> env.run() - >>> table, cols = create_table() - >>> df = pd.DataFrame(table, cols) - - Args: - skip_locations (bool, optional): If location states should be ignored. - Defaults to True. - save_static (bool, optional): If non-recording states are saved. - Defaults to False. - - Returns: - list[STATE_DATA_ROW]: Data table - list[str]]: Column names. - """ - _base = UpstageBase() - data: list[tuple[Any, ...]] = [] - seen_resources: list[Any] = [] - for actor in _base.get_actors(): - name = actor.name - kind = actor.__class__.__name__ - _data, _resources = _actor_state_data( - actor, skip_locations=skip_locations, save_static=save_static - ) - seen_resources.extend(_resources) - data.extend(_data) - - for monitoring in _base.get_monitored(): - if monitoring in seen_resources: - continue - name = f"{monitoring.name}" - kind = f"{monitoring.__class__.__name__}" - rows = [(name, kind, "Resource", t, value, None) for t, value in monitoring._quantities] - data.extend(rows) - - colnames = COLUMN_NAMES + ["Value", ACTIVATION_STATUS_COL] - return data, colnames - - -def create_location_table() -> tuple[list[LOCATION_DATA_ROW], list[str]]: - """Create a data table of every location UPSTAGE has recorded. - - Assumes that all location types are the same. - - This uses the current environment context. - - Usage: - - >>> import pandas as pd - >>> with UP.EnvironmentContext() as env: - >>> ... - >>> env.run() - >>> table, cols = create_location_table() - >>> df = pd.DataFrame(table, cols) - - Returns: - list[LOCATION_DATA_ROW]: Data table - list[str]]: Column names. - """ - _base = UpstageBase() - data: list[LOCATION_DATA_ROW] = [] - for actor in _base.get_actors(): - _data, cols = _actor_location_data(actor) - data.extend(_data) - return data, COLUMN_NAMES + cols + [ACTIVATION_STATUS_COL] diff --git a/src/upstage_des/events.py b/src/upstage_des/events.py deleted file mode 100644 index 62b44eb..0000000 --- a/src/upstage_des/events.py +++ /dev/null @@ -1,897 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Classes for UPSTAGE events that feed to simpy.""" - -from collections.abc import Callable -from contextlib import suppress -from typing import Any as tyAny -from warnings import warn - -import simpy as SIM -from simpy.resources.container import ContainerGet, ContainerPut -from simpy.resources.resource import Release, Request -from simpy.resources.store import StoreGet, StorePut - -from .base import SIMPY_GEN, SimulationError, UpstageBase, UpstageError -from .constants import PLANNING_FACTOR_OBJECT -from .units import unit_convert - -__all__ = ( - "All", - "Any", - "BaseEvent", - "Event", - "Get", - "FilterGet", - "MultiEvent", - "Put", - "ResourceHold", - "Wait", -) - -SIM_REQ_EVTS = ContainerGet | ContainerPut | StoreGet | StorePut | Request | Release - - -class BaseEvent(UpstageBase): - """Base class for framework events.""" - - def __init__(self, *, rehearsal_time_to_complete: float = 0.0): - """Create a base event with a notion of rehearsal time. - - Args: - rehearsal_time_to_complete (float, optional): Time to simulate passing - on rehearsal. Defaults to 0.0. - """ - super().__init__() - self._simpy_event: SIM.Event | None = None - self._rehearsing: bool = False - self._done_rehearsing: bool = False - - self.created_at: float = self.now - self.rehearsal_time_to_complete = rehearsal_time_to_complete - - @property - def now(self) -> float: - """Current sim time. - - Returns: - float: sim time - """ - return self.env.now - - def calculate_time_to_complete(self) -> float: - """Calculate the time elapsed until the event is triggered. - - Returns: - float: The time until the event triggers. - """ - return self.rehearsal_time_to_complete - - def as_event(self) -> SIM.Event: - """Convert UPSTAGE event to a simpy Event. - - Returns: - SIM.Event: The upstage event as a simpy event. - """ - raise NotImplementedError( - "Events must specify how to convert to :class:`simpy.events.Event`" - ) - - def is_complete(self) -> bool: - """Is the event complete? - - Returns: - bool: If it's complete or not. - """ - if self._rehearsing: - return self._done_rehearsing - if self._simpy_event is None: - raise UpstageError("Event has no simpy equivalent made.") - return self._simpy_event.processed - - def cancel(self) -> None: - """Cancel an event.""" - raise NotImplementedError("Implement custom event cancelling") - - @property - def rehearsing(self) -> bool: - """If the event is rehearsing. - - Returns: - bool - """ - return self._rehearsing - - @property - def done_rehearsing(self) -> bool: - """If the event is done rehearsing. - - Returns: - bool - """ - return self._done_rehearsing - - def _start_rehearsal(self) -> None: - """Set the event to testing mode.""" - self._rehearsing = True - self._done_rehearsing = False - - def _finish_rehearsal(self, complete: bool) -> None: - """Finish rehearsing the event. - - Args: - complete (bool): Indicates if the event was successful during the test. - """ - if not self._rehearsing: - raise SimulationError( - "Trying to finish testing event but event testing was not started`" - ) - self._done_rehearsing = complete - - def rehearse(self) -> tuple[float, tyAny | None]: - """Run the event in 'rehearsal' mode without changing the real environment. - - This is used by the task rehearsal functions. - - Returns: - tuple[float, Any | None]: The time to complete and the event's response. - """ - self._start_rehearsal() - time_advance = self.calculate_time_to_complete() - self._finish_rehearsal(complete=True) - - event_response = None - - return time_advance, event_response - - -class Wait(BaseEvent): - """Wait a specified or random uniformly distributed amount of time. - - Return a timeout. If time is a list of length 2, choose a random time - between the interval given. - - Rehearsal time is given by the maximum time of the interval, if given. - - Parameters - ---------- - timeout : int, float, list, tuple - Amount of time to wait. If it is a list or a tuple of length 2, a - random uniform value between the two values will be used. - - """ - - def _convert_time(self, time: float | int, unit: str | None) -> float: - """Convert a time to the stage time. - - Args: - time (float | int): The current time - unit (str): Units the time is in - - Returns: - float: Time in stage units - """ - base_unit = self.stage.get("time_unit") - if base_unit is not None and unit is not None: - return unit_convert(time, unit, base_unit) - return time - - def __init__( - self, - timeout: float | int, - timeout_unit: str | None = None, - *, - rehearsal_time_to_complete: float | int | None = None, - ) -> None: - """Create a timeout event. - - If timeout_unit is specified, UPSTAGE will try to convert it to the - time_unit set in the stage. Otherwise, it defaults to that time unit. - - Args: - timeout (float | int): Time to wait. - timeout_unit (str, optional): Units of time - rehearsal_time_to_complete (float | int, optional): The rehearsal time - to complete. Defaults to None (the timeout given). - - """ - if not isinstance(timeout, float | int): - raise SimulationError("Bad timeout. Did you mean to use from_random_uniform?") - timeout = self._convert_time(timeout, timeout_unit) - self._time_to_complete = timeout - self.timeout = timeout - if self._time_to_complete < 0: - raise SimulationError(f"Negative timeout in Wait: {self._time_to_complete}") - rehearse = timeout if rehearsal_time_to_complete is None else rehearsal_time_to_complete - super().__init__(rehearsal_time_to_complete=rehearse) - self._simpy_event: SIM.Timeout | None = None - - @classmethod - def from_random_uniform( - cls, - low: float | int, - high: float | int, - timeout_unit: str | None = None, - *, - rehearsal_time_to_complete: float | int | None = None, - ) -> "Wait": - """Create a wait from a random uniform time. - - If timeout_unit is specified, UPSTAGE will try to convert it to the - time_unit set in the stage. Otherwise, it defaults to that time unit. - - Args: - low (float): Lower bounds of random draw - high (float): Upper bounds of random draw - timeout_unit (str, optional): Units of time - rehearsal_time_to_complete (float | int, optional): The rehearsal time - to complete. Defaults to None - meaning the random value drawn. - - Returns: - Wait: The timeout event - """ - rng = UpstageBase().stage.random - timeout = rng.uniform(low, high) - return cls(timeout, timeout_unit, rehearsal_time_to_complete=rehearsal_time_to_complete) - - def as_event(self) -> SIM.Timeout: - """Cast Wait event as a simpy Timeout event. - - Returns: - SIM.Timeout - """ - assert isinstance(self.env, SIM.Environment) - if self._simpy_event is None: - self._simpy_event = self.env.timeout(self._time_to_complete) - return self._simpy_event - - def cancel(self) -> None: - """Cancel the timeout. - - There's no real meaning to cancelling a timeout. It sits in simpy's queue either way. - """ - assert self._simpy_event is not None - try: - self._simpy_event.defused = True - except RuntimeError as exc: - warn(f"Runtime error when cancelling '{self}', Error: {exc}!") - - -class BaseRequestEvent(BaseEvent): - """Base class for Request Events. - - Requests are things like Get and Put that wait in a queue. - """ - - def __init__(self, rehearsal_time_to_complete: float = 0.0) -> None: - """Create a request event. - - Args: - rehearsal_time_to_complete (float, optional): Estimated time to complete. - Defaults to 0.0. - """ - super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) - self._request_event: SIM_REQ_EVTS | None = None - - def cancel(self) -> None: - """Cancel the Request.""" - if self._request_event is None: - return - if not self.is_complete(): - self._request_event.cancel() - # Note: inherited classes need to deal with put-backs. - - def is_complete(self) -> bool: - """Test if the request is finished. - - Returns: - bool - """ - if self.rehearsing: - if self.done_rehearsing is None: - raise SimulationError( - f"Event '{self}' rehearsal started, but completion was" - "not set as incomplete, i.e., to `False`!" - ) - return self.done_rehearsing - assert self._request_event is not None - return self._request_event.processed - - -class Put(BaseRequestEvent): - """Wrap the ``simpy`` Put event. - - This is an event that puts an object into a ``simpy`` store or puts - an amount into a container. - - """ - - def __init__( - self, - put_location: SIM.Container | SIM.Store, - put_object: float | int | tyAny, - rehearsal_time_to_complete: float = 0.0, - ) -> None: - """Create a Put request for a store or container. - - Args: - put_location (SIM.Container | SIM.Store): Any container, store, or subclass. - put_object (float | int | Any): The amount (float | int) or object (Any) to put. - rehearsal_time_to_complete (float, optional): Estimated time for the put to finish. - Defaults to 0.0. - """ - super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) - - if not issubclass(put_location.__class__, SIM.Container | SIM.Store): - raise SimulationError( - f"put_location must be a subclass of Container " - f"or Store, not {put_location.__class__}" - ) - - self.put_location = put_location - self.put_object = put_object - self._request_event: ContainerPut | StorePut | None = None - - def as_event(self) -> ContainerPut | StorePut: - """Convert event to a ``simpy`` Event. - - Returns: - --------- - :obj:`simpy.events.Event` - Put request as a simpy event. - - """ - if self._request_event is None: - self._request_event = self.put_location.put(self.put_object) - return self._request_event - - -class MultiEvent(BaseEvent): - """A base class for evaluating multiple events. - - Note: - Subclasses of MultiEvent must define these methods: - * aggregation_function: Callable[[list[float]], float] - * simpy_equivalent: simpy.Event - - For an example, refer to :class:`~Any` and :class:`~All`. - """ - - def __init__(self, *events: BaseEvent | SIM.Process) -> None: - """Create a multi-event based on a list of events. - - Args: - *events (BaseEvent): The events that comprise the multi-event. - """ - super().__init__() - - for event in events: - if not issubclass(event.__class__, BaseEvent): - warn( - f"Event '{event}' is not an upstage Event. " - f"All events in a MultiEvent must be an " - f"instance of upstage BaseEvent if you are going " - f"to rehearse the task that contains this MultiEvent.", - UserWarning, - ) - self.events = events - self._simpy_event = None - - @staticmethod - def aggregation_function(times: list[float]) -> float: - """Aggregate event times to one single time. - - Args: - times (list[float]): Event rehearsal times - - Returns: - float: The aggregated time - """ - raise NotImplementedError("Implement in subclass") - - @staticmethod - def simpy_equivalent(env: SIM.Environment, events: list[SIM.Event]) -> SIM.Event: - """Return the simpy equivalent event. - - Args: - env (SIM.Environment): The SimPy environment. - events (list[BaseEvent]): Events to turn into multi-event. - - Returns: - SIM.Event: The aggregate event. - """ - raise NotImplementedError("Implement in subclass") - - def _make_event(self, event: BaseEvent | SIM.Process) -> SIM.Event: - # handle a process in the MultiEvent for non-rehearsal uses - if isinstance(event, SIM.Process): - return event - return event.as_event() - - def as_event(self) -> SIM.Event: - """Convert the UPSTAGE event to simpy. - - Returns: - SIM.Event: typically an Any or All - """ - sub_events = [self._make_event(event) for event in self.events] - assert isinstance(self.env, SIM.Environment) - self._simpy_event = self.simpy_equivalent(self.env, sub_events) - return self._simpy_event - - def cancel(self) -> None: - """Cancel the multi event and propagate it to the sub-events.""" - if self._simpy_event is None: - raise UpstageError("Can't cancel a nonexistent event.") - self._simpy_event.defused = True - self._simpy_event.fail(Exception("defused")) - for event in self.events: - if isinstance(event, BaseEvent): - try: - event.cancel() - except Exception as e: - msg = f"Event {event} in {self} failed to cancel\n\t:{e}" - raise SimulationError(msg) - - def calculate_time_to_complete( - self, - ) -> float: - """Compute time required to complete the multi-event. - - Args: - return_sub_events (bool, Optional): Whether to return all times or not. - Defaults to False. - """ - event_times = { - event: event.calculate_time_to_complete() - for event in self.events - if isinstance(event, BaseEvent) - } - - time_to_complete = self.aggregation_function(list(event_times.values())) - - return time_to_complete - - def calc_time_to_complete_with_sub(self) -> tuple[float, dict[BaseEvent, float]]: - """Compute time required for MultiEvent and get sub-event times. - - Returns: - tuple[float, dict[BaseEvent, float]]: Aggregate and individual times. - """ - event_times = { - event: event.calculate_time_to_complete() - for event in self.events - if isinstance(event, BaseEvent) - } - time_to_complete = self.aggregation_function(list(event_times.values())) - - return time_to_complete, event_times - - def _start_rehearsal(self) -> None: - """Start rehearsing all the sub-events.""" - super()._start_rehearsal() - for event in self.events: - if not hasattr(event, "_start_rehearsal"): - raise SimulationError( - f"Event '{event}' is not an upstage Event. " - f"All events in a MultiEvent must be an " - f"instance of upstage BaseEvent if you are going" - f"to rehearse the task that contains this MultiEvent." - ) - event._start_rehearsal() - - def rehearse(self) -> tuple[float, tyAny]: - """Run the event in 'trial' mode without changing the real environment. - - Returns: - tuple[float, Any]: The time to complete and the event's response. - - Note: - This is used by the task rehearsal functions. - """ - self._start_rehearsal() - - event_response = None - time_to_finish, event_times = self.calc_time_to_complete_with_sub() - - for event, event_end_time in event_times.items(): - event._finish_rehearsal(complete=event_end_time <= time_to_finish) - - self._finish_rehearsal(complete=True) - - return time_to_finish, event_response - - -class Any(MultiEvent): - """An event that requires one event to succeed before succeeding.""" - - @staticmethod - def aggregation_function(times: list[float]) -> float: - """Aggregation function for rehearsal time. - - Args: - times (list[float]): List of rehearsal times - - Returns: - float: Aggregated time (the minimum) - """ - return min(times) - - @staticmethod - def simpy_equivalent(env: SIM.Environment, events: list[SIM.Event]) -> SIM.Event: - """Return the SimPy version of the UPSTAGE Any event. - - Args: - env (SIM.Environment): SimPy Environment. - events (list[SIM.Event]): List of events. - - Returns: - SIM.Event: A simpy AnyOf event. - """ - return SIM.AnyOf(env, events) - - -class Get(BaseRequestEvent): - """Wrap the ``simpy`` Get event. - - Event that gets an object from a ``simpy`` store or gets an amount from a - container. - """ - - def __init__( - self, - get_location: SIM.Store | SIM.Container, - *get_args: tyAny, - rehearsal_time_to_complete: float = 0.0, - **get_kwargs: tyAny, - ) -> None: - """Create a Get request on a store, container, or subclass of those. - - Args: - get_location (SIM.Store | SIM.Container): The place for the Get request - rehearsal_time_to_complete (float, optional): _description_. Defaults to 0.0. - get_args (Any): optional positional args for the get request - (blank for Store and Container) - get_kwargs (Any): optional keyword args for the get request - (blank for Store and Container) - """ - super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) - - if not issubclass(get_location.__class__, SIM.Container | SIM.Store): - raise SimulationError( - "'put_location' must be a subclass of Container" - f" or Store, not {get_location.__class__}" - ) - - self.get_location = get_location - self.get_args = get_args - self.get_kwargs = get_kwargs - self.__is_store = issubclass(get_location.__class__, SIM.Store) - self._request_event: ContainerGet | StoreGet | None = None - - def calculate_time_to_complete(self) -> float: - """Calculate time elapsed until the event is triggered. - - Returns: - float: Estimated time until the event triggers. - - """ - return self.rehearsal_time_to_complete - - def as_event(self) -> ContainerGet | StoreGet: - """Convert get to a ``simpy`` Event. - - Returns: - ContainerGet | StoreGet - """ - # TODO: optional checking for container types for feasibility - if self._request_event is None: - self._request_event = self.get_location.get( - *self.get_args, - **self.get_kwargs, - ) - return self._request_event - - def get_value(self) -> tyAny: - """Get the value returned when the request is complete. - - Returns: - Any: The amount or item requested. - """ - if self.__is_store: - if self.rehearsing and self.done_rehearsing: - return PLANNING_FACTOR_OBJECT - if self._request_event is not None: - try: - return self._request_event.value - except AttributeError: - raise SimulationError("Requested item from an unfinished Get request.") - else: - raise SimulationError("Requested item from an unfinished Get request.") - else: - raise SimulationError( - "'get_value' is not supported for Containers. Check is_" - "complete and use the amount you requested." - ) - - def rehearse(self) -> tuple[float, tyAny]: - """Mock the event to test if it is feasible. - - Note: - The function does not fully test the conditions to satisfy the - get request, but this method can be called as part of a more - complex rehearse run. - - Returns: - float: The time it took to do the request - Any: The value of the request. - """ - time_advance, _ = super().rehearse() - event_response = None - if self.__is_store: - event_response = PLANNING_FACTOR_OBJECT - return time_advance, event_response - - def cancel(self) -> None: - """Cancel the get, and check if we got the item. - - There is an edge case where a Get request has the item, but - isn't given back to the process because an interrupt sorts - to first in the queue. This method handles that edge - case, giving the item back. - """ - super().cancel() - # Return the item if we got it. - if isinstance(self._request_event, ContainerGet | StoreGet): - with suppress(SimulationError): - value = self.get_value() - if value is PLANNING_FACTOR_OBJECT: - return - - def _putter() -> SIMPY_GEN: - yield self.get_location.put(value) - - self.env.process(_putter()) - - -class ResourceHold(BaseRequestEvent): - """Wrap the ``simpy`` request resource event. - - This manages getting and giving back all in one object. - - Example: - >>> resource = simpy.Resource(env, capacity=1) - >>> hold = ResourceHold(resource) - >>> # yield on the hold to get it - >>> yield hold - >>> # now that you have it, do things.. - >>> # give it back - >>> yield hold - >>> ... - """ - - def __init__( - self, - resource: SIM.Resource, - *resource_args: tyAny, - rehearsal_time_to_complete: float = 0.0, - **resource_kwargs: tyAny, - ) -> None: - """Create an event to use twice to get and give back a resource. - - Args: - resource (SIM.Resource): The simpy resource object. - rehearsal_time_to_complete (float, optional): Expected time to wait to - get the resource. Defaults to 0.0. - *resource_args (Any): positional arguments to the resource - **resource_kwargs (Any): keyword arguments to the resource - """ - super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) - - self.resource = resource - self.resource_args = resource_args - self.resource_kwargs = resource_kwargs - self._stage = "request" - self._request: Request | Release | None = None - - def calculate_time_to_complete(self) -> float: - """Time to complete, based on waiting for getting or giving back. - - Returns: - float: Time - """ - if self._stage == "request": - # assume the stage will switch on the next call - self._stage = "release" - return self.rehearsal_time_to_complete - elif self._stage == "release": - return 0.0 - raise UpstageError(f"Resource request stage is wrong: {self._stage}") - - def as_event(self) -> Request | Release: - """Create the simpy event for the right state of Resource usage. - - Returns: - Request | Release: The simpy event. - """ - if self._stage == "request": - self._request = self.resource.request(*self.resource_args, **self.resource_kwargs) - - self._request_event = self._request - self._stage = "release" - return self._request_event - elif self._stage == "release": - if not self._request or not self._request.processed: - raise SimulationError( - "Resource release requested when the " - "resource hasn't been given. Did you cancel?" - ) - assert isinstance(self._request, Request) - self._request_event = self.resource.release(self._request) - self._stage = "completed" - return self._request_event - raise UpstageError(f"Bad stage for Resource Hold: {self._stage}") - - -class FilterGet(Get): - """A Get for a FilterStore.""" - - def __init__( - self, - get_location: SIM.FilterStore, - filter: Callable[[tyAny], bool], - rehearsal_time_to_complete: float = 0.0, - ) -> None: - """Create a Get request on a FilterStore. - - The filter function returns a boolean (in/out of consideration). - - Args: - get_location (SIM.Store | SIM.Container): The place for the Get request - filter (Callable[[Any], bool]): The function that filters items in the store - rehearsal_time_to_complete (float, optional): _description_. Defaults to 0.0. - """ - super().__init__( - get_location=get_location, - rehearsal_time_to_complete=rehearsal_time_to_complete, - filter=filter, - ) - - -class All(MultiEvent): - """An event that requires all events to succeed before succeeding.""" - - @staticmethod - def aggregation_function(times: list[float]) -> float: - """Aggregate event times for rehearsal. - - Args: - times (list[float]): List of rehearsing times. - - Returns: - float: Aggregated (maximum) time. - """ - return max(times) - - @staticmethod - def simpy_equivalent(env: SIM.Environment, events: list[SIM.Event]) -> SIM.Event: - """Return the SimPy version of the UPSTAGE All event. - - Args: - env (SIM.Environment): SimPy Environment. - events (list[SIM.Event]): List of events. - - Returns: - SIM.Event: A simpy AllOf event. - """ - return SIM.AllOf(env, events) - - -class Event(BaseEvent): - """An UPSTAGE version of the standard SimPy Event. - - Returns a planning factor object on rehearsal for user testing against in rehearsals, in case. - - When the event is succeeded, a payload can be added through kwargs. - - This Event assumes that it might be long-lasting, and will auto-reset when yielded on. - """ - - def __init__( - self, - rehearsal_time_to_complete: float = 0.0, - auto_reset: bool = True, - ) -> None: - """Create an event. - - Args: - rehearsal_time_to_complete (float, optional): Expected time to complete. - Defaults to 0.0. - auto_reset (bool, optional): Whether to auto-reset on yield. Defaults to True. - """ - super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) - # The usage is sometimes that events might succeed before being - # yielded on - self._payload: dict[str, Any] = {} - self._auto_reset = auto_reset - assert isinstance(self.env, SIM.Environment) - self._event = SIM.Event(self.env) - - def calculate_time_to_complete(self) -> float: - """Return the time to complete. - - Returns: - float: Time to complete estimate. - """ - return self.rehearsal_time_to_complete - - def as_event(self) -> SIM.Event: - """Get the Event as a simpy type. - - This resets the event if allowed. - - Returns: - SIM.Event - """ - if self.is_complete(): - if self._auto_reset: - self.reset() - else: - raise UpstageError("Event not allowed to reset on yield.") - return self._event - - def succeed(self, **kwargs: tyAny) -> None: - """Succeed the event and store any payload. - - Args: - **kwargs (Any): key:values to store as payload. - """ - if self.is_complete(): - raise SimulationError("Event has already completed") - self._payload = kwargs - self._event.succeed() - - def is_complete(self) -> bool: - """Is the event done? - - Returns: - bool - """ - return self._event.processed - - def get_payload(self) -> dict[str, tyAny]: - """Get any payload from the call to succeed(). - - Returns: - dict[str, Any]: The payload left by the succeed() caller. - """ - return self._payload - - def reset(self) -> None: - """Reset the event to allow it to be held again.""" - assert isinstance(self.env, SIM.Environment) - self._event = SIM.Event(self.env) - - def cancel(self) -> None: - """Cancel the event. - - Cancelling doesn't mean much, since it's still going to be yielded on. - """ - try: - self._event.defused = True - self._event.succeed() - except RuntimeError as exc: - exc.add_note(f"Runtime error when cancelling '{self}'") - raise exc - - def rehearse(self) -> tuple[float, tyAny]: - """Run the event in 'trial' mode without changing the real environment. - - Returns: - tuple[float, Any]: The time to complete and the event's response. - """ - time_advance, _ = super().rehearse() - return time_advance, PLANNING_FACTOR_OBJECT diff --git a/src/upstage_des/geography/__init__.py b/src/upstage_des/geography/__init__.py deleted file mode 100644 index 70bc78f..0000000 --- a/src/upstage_des/geography/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Geographical methods for intersections, distances, and locations.""" - -from .geo_types import ( - INTERSECTION_LOCATION_CALLABLE, - LAT_LON, - LAT_LON_ALT, - EarthProtocol, -) -from .intersections import ( - CrossingCondition, - get_intersection_locations, -) -from .spherical import Spherical -from .wgs84 import WGS84 - -__all__ = [ - "EarthProtocol", - "Spherical", - "WGS84", - "get_intersection_locations", - "LAT_LON", - "LAT_LON_ALT", - "CrossingCondition", - "INTERSECTION_LOCATION_CALLABLE", -] diff --git a/src/upstage_des/geography/conversions.py b/src/upstage_des/geography/conversions.py deleted file mode 100644 index eb846cb..0000000 --- a/src/upstage_des/geography/conversions.py +++ /dev/null @@ -1,254 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Geodetic frame conversions.""" - -from math import atan2, cos, degrees, radians, sin, sqrt - -from .geo_types import POSITION, POSITIONS - -# Constants for the earth -spherical_radius: float = 6378137.0 -WGS84_A: float = 6378137.0 # meters - semimajor axis -WGS84_F: float = 1 / 298.257223563 # flattening factor -WGS84_B: float = WGS84_A * (1 - WGS84_F) # meters - semiminor axis - -# build the constant values -sR_e = spherical_radius -sR_p = spherical_radius -se_2 = (sR_e**2 - sR_p**2) / sR_e**2 -sep_2 = (sR_e**2 - sR_p**2) / sR_p**2 -se = sqrt(se_2) -sep = sqrt(se) - -wR_e = WGS84_A -wR_p = WGS84_B -# first eccentricity squared -we_2 = WGS84_F * (2 - WGS84_F) -# second eccentricity squared -wep_2 = we_2 / (1 - WGS84_F) ** 2 -we = sqrt(we_2) -wep = sqrt(we) -wF = WGS84_F - - -class BaseConversions: - """Base class for converting in geodetic frames.""" - - radius_e = 0.0 - radius_p = 0.0 - e_2 = 0.0 - ep_2 = 0.0 - e = 0.0 - ep = 0.0 - f = 0.0 - - @classmethod - def _lla2ecef(cls, lla: POSITION, input_in_radians: bool = False) -> POSITION: - """Convert Lat-Lon-Altitude into ECEF. - - Args: - lla (POSITION): lat/lon/alt data in 3 columns and 1 row. - input_in_radians (bool, optional): If the lat/lon are in radians. Defaults to False. - - Returns: - POSITION: XYZ ECEF value for the position. - """ - rad_lat, rad_lon, ht = lla - if not input_in_radians: - rad_lat = radians(rad_lat) # sometimes denoted as lambda - rad_lon = radians(rad_lon) # sometimes denoted as phi - - N = cls.radius_e / sqrt(1 - (cls.e_2 * sin(rad_lat) ** 2)) - temp1 = (N + ht) * cos(rad_lat) - temp2 = N * (1 - cls.f) ** 2 + ht - x = temp1 * cos(rad_lon) - y = temp1 * sin(rad_lon) - z = temp2 * sin(rad_lat) - return (x, y, z) - - @classmethod - def lla2ecef(cls, lla: POSITIONS, input_in_radians: bool = False) -> POSITIONS: - """Convert Lat-Lon-Altitude into ECEF. - - Args: - lla (POSITIONS): lat/lon/alt data in 3 columns and N rows. - input_in_radians (bool, optional): If the lat/lon are in radians. Defaults to False. - - Returns: - POSITIONS: Array of XYZ ECEF values. - """ - return [cls._lla2ecef(row, input_in_radians) for row in lla] - - @classmethod - def _ecef2lla(cls, ecef: POSITION, radians_out: bool = False) -> POSITION: - """Convert ECEF to Lat/Lon/Altitude. - - Args: - ecef (POSITION): ECEF point - radians_out (bool, optional): If lat/lon out should be in radians. Defaults to False. - - Returns: - POSITION: Lat/Lon/Altitude - """ - X, Y, Z = ecef - # Longitude (rad; 1xN) - lon = atan2(Y, X) - - # Calculations - e2 = cls.e_2 - r2 = X**2 + Y**2 - r = sqrt(r2) - a = cls.radius_e - b = cls.radius_p - a2 = a**2 - b2 = b**2 - E2 = a2 - b2 - Z2 = Z**2 - F = 54 * b2 * Z2 - G = r2 + (1 - e2) * Z2 - e2 * E2 - c = (e2 * e2 * F * r2) / (G * G * G) - - s = (1 + c + sqrt(c * c + 2 * c)) ** (1 / 3) - P = F / (3 * (s + 1 / s + 1) ** 2 * G * G) - Q = sqrt(1 + 2 * e2 * e2 * P) - ro = -(e2 * P * r) / (1 + Q) + sqrt( - (a * a / 2) * (1 + 1 / Q) - ((1 - e2) * P * Z2) / (Q * (1 + Q)) - P * r2 / 2 - ) - tmp = (r - e2 * ro) ** 2 - U = sqrt(tmp + Z2) - V = sqrt(tmp + (1 - e2) * Z2) - zo = (b2 * Z) / (a * V) - - h = U * (1 - b2 / (a * V)) - lat = atan2((Z + cls.ep_2 * zo), r) - - if not radians_out: - lon = degrees(lon) - lat = degrees(lat) - - lla = (lat, lon, h) - return lla - - @classmethod - def ecef2lla(cls, ecef: POSITIONS, radians_out: bool = False) -> POSITIONS: - """Convert ECEF to Lat/Lon/Altitude. - - Args: - ecef (POSITIONS): ECEF points - radians_out (bool, optional): If lat/lon out should be in radians. Defaults to False. - - Returns: - POSITIONS: Lat/Lon/Altitude - """ - return [cls._ecef2lla(row, radians_out) for row in ecef] - - -class SphericalConversions(BaseConversions): - """Conversions on a spherical globe.""" - - radius_e = sR_e - radius_p = sR_p - e_2 = se_2 - ep_2 = sep_2 - e = se - ep = sep - - @classmethod - def _lla2ecef(cls, lla: POSITION, input_in_radians: bool = False) -> POSITION: - """Lat-Lon-Alt to ECEF. - - Args: - lla (POSITION): Lat-Lon-Altitude points - input_in_radians (bool, optional): If lat/lon are in radians. Defaults to False. - - Returns: - POSITION: ECEF - """ - rad_lat = lla[0] - rad_lon = lla[1] - if not input_in_radians: - rad_lat = radians(rad_lat) # sometimes denoted as lambda - rad_lon = radians(rad_lon) # sometimes denoted as phi - - ht = lla[2] - rad = cls.radius_e + ht - clat = cos(rad_lat) - x = rad * cos(rad_lon) * clat - y = rad * clat * sin(rad_lon) - z = rad * sin(rad_lat) - return (x, y, z) - - @classmethod - def lla2ecef(cls, lla: POSITIONS, input_in_radians: bool = False) -> POSITIONS: - """Lat-Lon-Alt to ECEF. - - Args: - lla (POSITIONS): Lat-Lon-Altitude points - input_in_radians (bool, optional): If lat/lon are in radians. Defaults to False. - - Returns: - POSITIONS: ECEF - """ - return [cls._lla2ecef(row, input_in_radians) for row in lla] - - @classmethod - def _ecef2lla(cls, ecef: POSITION, radians_out: bool = False) -> POSITION: - """Convert ECEF to Lat-Lon-Alt. - - Args: - ecef (POSITION): Points in ECEF - radians_out (bool, optional): If Lat/Lon out are in radians. Defaults to False. - - Returns: - POSITION: Lat-Lon-Alt points - """ - x, y, z = ecef - # Longitude (rad) - lon = atan2(y, x) - - p = sqrt(x**2 + y**2) - # Latitude (rads) - lat = atan2(z, p) - h = p / cos(lat) - cls.radius_e - - # correct for numerical instability in altitude near exact poles: - # (after this correction, error is about 2 millimeters, which is about - # the same as the numerical precision of the overall function) - k = (abs(x) < 1) & (abs(y) < 1) - if k: - h = abs(z) - cls.radius_p - - if not radians_out: - lon = degrees(lon) - lat = degrees(lat) - - lla = (lat, lon, h) - return lla - - @classmethod - def ecef2lla(cls, ecef: POSITIONS, radians_out: bool = False) -> POSITIONS: - """Convert ECEF to Lat-Lon-Alt. - - Args: - ecef (POSITIONS): Points in ECEF - radians_out (bool, optional): If Lat/Lon out are in radians. Defaults to False. - - Returns: - POSITIONS: Lat-Lon-Alt points - """ - return [cls._ecef2lla(row, radians_out) for row in ecef] - - -class WGS84Conversions(BaseConversions): - """WGS84 coordinate conversions.""" - - radius_e = wR_e - radius_p = wR_p - e_2 = we_2 - ep_2 = wep_2 - e = we - ep = wep - f = wF diff --git a/src/upstage_des/geography/geo_types.py b/src/upstage_des/geography/geo_types.py deleted file mode 100644 index 56cc7e2..0000000 --- a/src/upstage_des/geography/geo_types.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Geographical types and protocols.""" - -from collections.abc import Callable -from dataclasses import dataclass -from typing import Protocol - -POSITION = tuple[float, float, float] -POSITIONS = list[POSITION] -LAT_LON_ALT = POSITION -LAT_LON = tuple[float, float] - - -class GEO_POINT(Protocol): - """Protocol for mimicking geographic data.""" - - def latlon(self) -> LAT_LON: - """Return a tuple of latitude and longitude in degrees.""" - - -def _convert_geo(point: LAT_LON | GEO_POINT) -> LAT_LON: - """Convert a geo point (or Lat/Lon) to Lat/Lon. - - Args: - point (LAT_LON | GEO_POINT): The position on a globe - - Returns: - LAT_LON: Position as correct type. - """ - if isinstance(point, tuple): - return point - return point.latlon() - - -class EarthProtocol(Protocol): - """Protocol for defining an earth model interface.""" - - def distance( - self, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - units: str, - ) -> float: - """Get the distance between two lat/lon (degrees) points.""" - - def bearing( - self, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - ) -> float: - """Get the distance between two lat/lon (degrees) points.""" - - def distance_and_bearing( - self, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - units: str, - ) -> tuple[float, float]: - """Get the distance between two lat/lon (degrees) points.""" - - def point_from_bearing_dist( - self, - point: LAT_LON | GEO_POINT, - bearing: float, - distance: float, - distance_units: str = "nmi", - ) -> tuple[float, float]: - """Get a lat/lon in degrees from a point, bearing, and distance.""" - - def lla2ecef( - self, - locs: list[LAT_LON_ALT], - ) -> list[tuple[float, float, float]]: - """Get ECEF coordinates from lat lon alt.""" - - def ecef2lla( - self, - locs: list[LAT_LON_ALT], - ) -> list[tuple[float, float, float]]: - """Get ECEF coordinates from lat lon alt.""" - - def geo_linspace( - self, - start: LAT_LON | GEO_POINT, - end: LAT_LON | GEO_POINT, - num_segments: int, - ) -> list[LAT_LON]: - """Get evenly spaced coordinates between lat/lon pairs.""" - - def geo_circle( - self, - center: LAT_LON | GEO_POINT, - radius: float, - radius_units: str, - num_points: int, - ) -> list[LAT_LON]: - """Create a circle on a globe.""" - - -@dataclass -class CrossingCondition: - """Data about an intersection.""" - - kind: str - begin: LAT_LON_ALT - end: LAT_LON_ALT | None = None - - -INTERSECTION_LOCATION_CALLABLE = Callable[ - [ - LAT_LON_ALT, - LAT_LON_ALT, - LAT_LON_ALT, - float, - str, - EarthProtocol, - float | None, - list[int] | None, - ], - list[CrossingCondition], -] diff --git a/src/upstage_des/geography/intersections.py b/src/upstage_des/geography/intersections.py deleted file mode 100644 index 802d55e..0000000 --- a/src/upstage_des/geography/intersections.py +++ /dev/null @@ -1,275 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Functions for finding intersections in geodetics.""" - -from upstage_des.math_utils import _vector_norm, _vector_subtract -from upstage_des.units import unit_convert - -from .geo_types import LAT_LON_ALT, POSITION, POSITIONS, CrossingCondition -from .spherical import Spherical -from .wgs84 import WGS84 - - -class IntersectionError(Exception): - """An exception when intersection code fails.""" - - ... - - -def _preprocess( - start_lla: LAT_LON_ALT, - finish_lla: LAT_LON_ALT, - point_lla: LAT_LON_ALT, - earth: Spherical | WGS84, - dist_between: float, -) -> tuple[POSITIONS, POSITIONS, POSITION]: - """Preprocess points for intersection calculations. - - Args: - start_lla (LAT_LON_ALT): Starting point (degrees/meters) - finish_lla (LAT_LON_ALT): Ending point (degrees/meters) - point_lla (LAT_LON_ALT): Point to convert to ecef (degrees/meters) - earth (Spherical | WGS84): Earth model - dist_between (float): Distance to use for segment length (m) - - Returns: - tuple[POSITIONS, POSITIONS, POSITION]: Preprocessed locations - """ - start_alt = start_lla[2] - finish_alt = finish_lla[2] - dist = earth.distance(start_lla[:2], finish_lla[:2], units="m") - points = int(dist / dist_between) + 1 - while points <= 2: - dist_between = dist_between / 2.0 - if dist_between < 100: - raise IntersectionError( - f"Intersetion Segment {start_lla} -> {finish_lla} is too small!" - ) - points = int(dist / dist_between) + 1 - ecef_point = earth.lla2ecef([point_lla])[0] - assert len(ecef_point) == 3 - ecef_test, geo_test = earth.ecef_and_geo_linspace( - start_lla[:2], - finish_lla[:2], - start_alt, - finish_alt, - points, - ) - return ecef_test, geo_test, ecef_point - - -def find_crossing_points( - start_lla: LAT_LON_ALT, - finish_lla: LAT_LON_ALT, - point_lla: LAT_LON_ALT, - earth: Spherical | WGS84, - radius: float, - dist_between: float = 9260.0, -) -> list[CrossingCondition]: - """Finds the points along a great circle path and a sphere. - - The output data provides booleans to state If the start or end are within - range/visibility. - - Args: - start_lla (LAT_LON_ALT): Starting point (degrees/meters) - finish_lla (LAT_LON_ALT): Ending point (degrees/meters) - point_lla (LAT_LON_ALT): Point of sensing (degrees/meters) - earth (Spherical | WGS84): Earth model - radius (float): Radius that the sensor can see (meters) - dist_between (float, optional): Distance to use for segment length (meters). - Defaults to 5 nmi (or 9260 meters). - - Returns: - list[CrossingCondition]: - A list of data describing the crossover points on the great circle path. - It will start with: ["START_INSIDE" or "START_OUT", start LLA]. - Then there will be one or two: ["ENTER" or "EXIT", LLA, LLA] where the - two LLA values are the OUT and IN points as described. - It will end with: ["END_INSIDE" or "END_OUT", end LLA]. - """ - ecef_test, lla_test, ecef_point = _preprocess( - start_lla, - finish_lla, - point_lla, - earth, - dist_between=dist_between, - ) - - last_out: int | None = None - last_in: int | None = None - cross_points: list[CrossingCondition] = [] - - for i, test_loc in enumerate(ecef_test): - diff = _vector_subtract(test_loc, ecef_point) - dist = _vector_norm(diff) - is_in = dist <= radius - - if i == 0: - use = "START_INSIDE" if is_in else "START_OUT" - cond = CrossingCondition(kind=use, begin=lla_test[i]) - cross_points.append(cond) - elif is_in and last_in != i - 1: - assert last_out is not None - cond = CrossingCondition( - kind="ENTER", - begin=lla_test[last_out], - end=lla_test[i], - ) - cross_points.append(cond) - assert last_out == i - 1 - elif not is_in and last_out != i - 1: - assert last_in is not None - cond = CrossingCondition( - kind="EXIT", - begin=lla_test[last_in], - end=lla_test[i], - ) - cross_points.append(cond) - assert last_in == i - 1 - - if is_in: - last_in = i - else: - last_out = i - - use = "END_INSIDE" if is_in else "END_OUT" - cond = CrossingCondition(kind=use, begin=lla_test[-1]) - cross_points.append(cond) - - return cross_points - - -def _split_down( - begin: LAT_LON_ALT, - end: LAT_LON_ALT, - sphere_center: LAT_LON_ALT, - radius: float, - earth: Spherical | WGS84, - distance_between: float, - subdivide_levels: list[int] | None = None, -) -> CrossingCondition: - """Find an intersection point from a sphere to a great circle. - - Default subdivision is [10, 20] - - We assume this is being called only on one clear crossing point, because it - expects to find START_(IN|OUT) -> (IN_OUT | OUT_IN) -> (END_(OUT|IN)) from the - calls to find_crossing_points - - Args: - begin (LAT_LON_ALT): Starting point (degrees/meters) - end (LAT_LON_ALT): Ending point (degrees/meters) - sphere_center (LAT_LON_ALT): Center of the sensing sphere (not earth, degrees/meters) - radius (float): Sensor radius (meters) - earth (Spherical | WGS84): Geodetic description - distance_between (float): Splitting distance for search - subdivide_levels (list[int], optional): Levels for searching smaller sections. - Defaults to None. - - Returns: - tuple[str, LAT_LON_ALT]: The intersection type, if any (degrees/meters) - """ - subdivide_levels = [10, 20] if subdivide_levels is None else subdivide_levels - - for divide in subdivide_levels: - distance_between = distance_between / divide - split_data = find_crossing_points( - begin, - end, - sphere_center, - earth, - radius, - distance_between, - ) - if len(split_data) != 3: - raise IntersectionError( - "A subdivide split shouldn't have 2 crossovers in intersections with a sphere" - ) - if split_data[1].kind not in ["EXIT", "ENTER"]: - raise IntersectionError("Subdividing an intersection check gave an invalid Direction") - assert split_data[1].end is not None - begin, end = split_data[1].begin, split_data[1].end - - cond = CrossingCondition( - split_data[1].kind, - begin=split_data[1].begin, - end=split_data[1].end, - ) - return cond - - -def get_intersection_locations( - start: LAT_LON_ALT, - finish: LAT_LON_ALT, - sphere_center: LAT_LON_ALT, - radius: float, - radius_units: str, - earth: WGS84 | Spherical, - dist_between: float | None = None, - subdivide_levels: list[int] | None = None, -) -> list[CrossingCondition]: - """Get the locations and kinds of intersections of a path to a sphere. - - Args: - start (LAT_LON_ALT): Starting point (degrees) - finish (LAT_LON_ALT): Ending point (degrees) - sphere_center (LAT_LON_ALT): Center of the sensing sphere (not earth, degrees/meters) - radius (float): Sensor radius - radius_units (str): Units of the sensor radius - earth (Spherical | WGS84): Geodetic description - dist_between (float, optional): Splitting distance for search. - Defaults to 9260.0 meters (5 nmi) - subdivide_levels (list[int], optional): Levels for searching smaller sections. - Defaults to None. - - Returns: - list[CrossingCondition]: The intersection type, if any - It will start with: - ["START_INSIDE" or "START_OUT"] - Then there will be one or two: - ["ENTER" or "EXIT"] - where the two LLA values are the OUT and IN points as described - It will end with: - ["END_INSIDE" or "END_OUT"] - """ - dist_between = 9260.0 if dist_between is None else dist_between - radius = unit_convert(radius, radius_units, "m") - subdivide_levels = [10, 20] if subdivide_levels is None else subdivide_levels - rough_split_data = find_crossing_points( - start, - finish, - sphere_center, - earth, - radius, - dist_between, - ) - - # subdivide the splits for precision - intersections: list[CrossingCondition] = [] - for condition in rough_split_data[1:-1]: - assert condition.end is not None - new_condition = _split_down( - condition.begin, - condition.end, - sphere_center, - radius, - earth, - dist_between, - subdivide_levels=subdivide_levels, - ) - if new_condition.kind != condition.kind: - raise IntersectionError("Subdividing the intersection switched directions") - use_lla = new_condition.begin if condition.kind == "EXIT" else new_condition.end - assert use_lla is not None - intersections.append(CrossingCondition(kind=condition.kind, begin=use_lla)) - - # for START_IN and END_IN, append that data, too - if rough_split_data[0].kind == "START_INSIDE": - intersections = [rough_split_data[0]] + intersections - if rough_split_data[-1].kind == "END_INSIDE": - intersections = intersections + [rough_split_data[-1]] - return intersections diff --git a/src/upstage_des/geography/spherical.py b/src/upstage_des/geography/spherical.py deleted file mode 100644 index efcd3e7..0000000 --- a/src/upstage_des/geography/spherical.py +++ /dev/null @@ -1,487 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Geographical math using spherical coordinates for Earth.""" - -from math import acos, asin, atan, atan2, cos, degrees, radians, sin, sqrt - -from upstage_des.math_utils import _vector_dot -from upstage_des.units import unit_convert - -from .conversions import SphericalConversions, spherical_radius -from .geo_types import GEO_POINT, LAT_LON, POSITION, POSITIONS, _convert_geo - - -class Spherical(SphericalConversions): - """Geographical math using spherical coordinates for Earth.""" - - EARTH_RADIUS = spherical_radius # m - - @staticmethod - def _bearing( - lat1: float, - lon1: float, - lat2: float, - lon2: float, - ) -> float: - ans = degrees( - atan2( - sin(lon2 - lon1) * cos(lat2), - cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(lon2 - lon1), - ) - ) - ans = ans % 360 - return float(ans) - - @classmethod - def bearing( - cls, - origin: LAT_LON | GEO_POINT, - destination: LAT_LON | GEO_POINT, - ) -> float: - """Calculate the forward bearing from origin to destination. - - Args: - origin (LAT_LON): Start lat/lon - destination (LAT_LON): End lat/lon - - Returns: - float: The forward bearing (0 is north) - """ - origin = _convert_geo(origin) - destination = _convert_geo(destination) - positions = [origin[1], origin[0], destination[1], destination[0]] - lon1, lat1, lon2, lat2 = map(radians, positions) - - return cls._bearing(lat1, lon1, lat2, lon2) - - @staticmethod - def _point_along( - d: float, - f: float, - lat1: float, - lon1: float, - lat2: float, - lon2: float, - ) -> LAT_LON: - """Get a point a fraction between two points on a great circle. - - Args: - d (float): Distance between the points (meters) - f (float): Fraction of the distance for the new point - lat1 (float): Latitude of the first point (radians) - lon1 (float): Longitude of the second point (radians) - lat2 (float): Latitude of the second point (radians) - lon2 (float): Longitude of the second point (radians) - - Returns: - LAT_LON: Lat and Lon (degrees) - """ - a = sin((1 - f) * d) / sin(d) - b = sin(f * d) / sin(d) - x = a * cos(lat1) * cos(lon1) + b * cos(lat2) * cos(lon2) - y = a * cos(lat1) * sin(lon1) + b * cos(lat2) * sin(lon2) - z = a * sin(lat1) + b * sin(lat2) - lat = degrees(atan2(z, sqrt(x**2 + y**2))) - lon = degrees(atan2(y, x)) - - return lat, lon - - @classmethod - def point_along( - cls, - origin: LAT_LON | GEO_POINT, - destination: LAT_LON | GEO_POINT, - f: float, - ) -> LAT_LON: - """Find the location fractionally along a great circle path. - - Args: - origin (LAT_LON): Lat / Lon (degrees) - destination (LAT_LON): Lat / Lon (degrees) - f (float): Fraction along the great circle path to get the point at - - Returns: - LAT_LON: Point between origin and destination (degrees) - """ - origin = _convert_geo(origin) - destination = _convert_geo(destination) - positions = [origin[1], origin[0], destination[1], destination[0]] - lon1, lat1, lon2, lat2 = map(radians, positions) - d = cls.distance(origin, destination, units="m") / spherical_radius - lat, lon = cls._point_along(d, f, lat1, lon1, lat2, lon2) - return (lat, lon) - - @classmethod - def _ll2cart(cls, lat: float, lon: float, radius: float = 1.0) -> POSITION: - """Latitude and longitude to cartesian on a spherical earth. - - Args: - lat (float | list[float]): Latitude - lon (float | list[float]): Longitude - radius (float, optional): Sphere radius. Defaults to 1. - - Returns: - POSITION: XYZ coordinates - """ - theta, lam = radians(lon), radians(lat) - cl = cos(lam) - v = (radius * cos(theta) * cl, radius * sin(theta) * cl, radius * sin(lam)) - return v - - @classmethod - def _cart2lla(cls, cart: POSITION, radius: float = 1.0) -> POSITION: - """Convert XYZ to lat/lon. - - Args: - cart (POSITION): Cartesian (XYZ) point. - radius (float, optional): Radius of the sphere. Defaults to 1.0. - - Returns: - POSITION: Latitude and longitude (degrees), and altitude (meters) - """ - a, b, c = cart - lam = asin(c) - theta = atan2(b, a) - alt = sum(x**2 for x in cart) - radius - return degrees(lam), degrees(theta), alt - - @classmethod - def geo_linspace( - cls, - start: LAT_LON | GEO_POINT, - end: LAT_LON | GEO_POINT, - num_segments: int = 10, - ) -> list[LAT_LON]: - """Make a discrete great circle path between two points. - - Altitudes are not considered since they do not affect angular coordinates. - - The number of points is the number - - Args: - start (LAT_LON): Start point - end (LAT_LON): End point - num_segments (int, optional): Number of segments on the path. Defaults to 10. - - Returns: - list[LAT_LON]: Lattiude and Longitude (degrees) - """ - start = _convert_geo(start) - end = _convert_geo(end) - lats, lons, _ = cls.geo_linspace_with_ecef(start, end, num_segments) - latlon: list[LAT_LON] = [(la, lo) for la, lo in zip(lats, lons)] - return latlon - - @classmethod - def geo_linspace_with_ecef( - cls, - start: LAT_LON | GEO_POINT, - end: LAT_LON | GEO_POINT, - num_segments: int = 10, - ) -> tuple[list[float], list[float], POSITIONS]: - """Make a discrete great circle path between two points. - - Altitudes are not considered since they do not affect angular coordinates. - - Latitude and Longitude are all in degrees - - Args: - start (LAT_LON): Starting lat/lon (degrees) - end (LAT_LON): Ending lat/lon (degrees) - num_segments (int, optional): Number of segments to make. Defaults to 10. - - Returns: - tuple[list[float], list[float], POSITIONS]: Latitude, Longitude, and ECEF points. - """ - start = _convert_geo(start) - end = _convert_geo(end) - if num_segments < 2: - raise ValueError("Not enough segments for interpolation") - # We keep the earth radius to be 1 since we're interpolating along radians only - v1 = cls._ll2cart(start[0], start[1]) - v2 = cls._ll2cart(end[0], end[1]) - # solve for the parameter value that equals the end point - d = _vector_dot(v1, v2) - common = sqrt(-1 / ((d - 1) * (d + 1))) - alpha, beta = -d * common, common - a, _, _ = v1 - d, _, _ = v2 - lead = a * alpha + beta * d - sqrt_inner = a**2 * alpha**2 + a**2 + 2 * a * alpha * beta * d + beta**2 * d**2 - d**2 - if sqrt_inner < 0: - raise ValueError("geo_linspace fails due to negative sqrt") - t = 2 * atan((lead + sqrt(sqrt_inner)) / (a + d)) - W = tuple([alpha * x + beta * y for x, y in zip(v1, v2)]) - assert len(W) == 3 - W = (W[0], W[1], W[2]) - # test that the t is right - ct, st = cos(t), sin(t) - end_pt = tuple([ct * x + st * w for x, w in zip(v1, W)]) - assert len(end_pt) == 3 - end_pt = (end_pt[0], end_pt[1], end_pt[2]) - is_close = all(abs(a - b) <= 1e-8 for a, b in zip(end_pt, v2)) - if not is_close: - t = 2 * atan((lead - sqrt(sqrt_inner)) / (a + d)) - - ts = [(t / num_segments) * i for i in range(0, num_segments + 1)] - v_cos = [tuple(cos(t) * v for v in v1) for t in ts] - w_sin = [tuple(sin(t) * w for w in W) for t in ts] - end_points: list[POSITION] = [] - for v, w in zip(v_cos, w_sin): - pt = (v[0] + w[0], v[1] + w[1], v[2] + w[2]) - end_points.append(pt) - - lla = cls.ecef2lla(end_points) - - return [lat for lat, *_ in lla], [lon for _, lon, _ in lla], end_points - - @classmethod - def geo_circle( - cls, - center: LAT_LON | GEO_POINT, - radius: float, - radius_units: str = "nmi", - num_points: int = 10, - ) -> list[LAT_LON]: - """Make an approximate circle by sweeping bearing from a central location. - - Altitudes are not considered since they do not affect angular coordinates. - - Args: - center (LAT_LON): Center point (degrees) - radius (float): Radius of the circle - radius_units (str, optional): Units of the radius. Defaults to 'nmi'. - num_points (int, optional): Number of points on the circle. Defaults to 10. - - Returns: - list[LAT_LON]: Latitude and Longitude (degrees) of the circle - """ - center = _convert_geo(center) - lats: list[float] = [] - lons: list[float] = [] - for fraction in range(0, num_points + 1): - f = fraction / num_points - bearing = f * 360 - lat, lon = cls.point_from_bearing_dist( - center, - bearing, - radius, - distance_units=radius_units, - ) - lats.append(lat) - lons.append(lon) - - latlon: list[LAT_LON] = [(la, lo) for la, lo in zip(lats, lons)] - return latlon - - @classmethod - def distance( - cls, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - units: str = "nmi", - ) -> float: - """Find distance between two points. - - Return the great circle distance from the loc1 to another loc2. - - Args: - loc1 (LAT_LON): Starting point (degrees) - loc2 (LAT_LON): Ending point (degrees) - units (str, optional): Units requested for distance. Defaults to 'nmi'. - - Returns: - float: Distances in the defined units - """ - loc1 = _convert_geo(loc1) - loc2 = _convert_geo(loc2) - positions = [loc1[1], loc1[0], loc2[1], loc2[0]] - lon1, lat1, lon2, lat2 = map(radians, positions) - a = sin(0.5 * (lat2 - lat1)) ** 2 + cos(lat1) * cos(lat2) * sin(0.5 * (lon2 - lon1)) ** 2 - - dist_m = spherical_radius * 2 * atan2(sqrt(a), sqrt(1.0 - a)) - return unit_convert(dist_m, "m", units) - - @classmethod - def distance_and_bearing( - cls, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - units: str = "nmi", - ) -> tuple[float, float]: - """Find great circle distance and forward bearing between two points. - - Args: - loc1 (LAT_LON): Starting point - loc2 (LAT_LON): Ending point - units (str, optional): Distance units requested. Defaults to 'nmi'. - - Returns: - tuple[float, float]: Distance and Bearing - """ - dist = cls.distance(loc1, loc2, units=units) - bearing = cls.bearing(loc1, loc2) - return dist, bearing - - @classmethod - def point_from_bearing_dist( - cls, - point: LAT_LON | GEO_POINT, - bearing: float, - distance: float, - distance_units: str = "nmi", - ) -> LAT_LON: - """Get a Location from a starting point, a bearing, and a distance. - - Args: - point (LAT_LON): Starting point (degrees) - bearing (float): Bearing to travel along (degrees) - distance (float): Distance to travel - distance_units (str, optional): Units of the distance. Defaults to 'nmi'. - - Returns: - LAT_LON: The point - """ - point = _convert_geo(point) - bearing = radians(bearing) - dist = unit_convert(distance, distance_units, "m") - - lat, lon = point - lat1 = radians(lat) - lon1 = radians(lon) - - lat2 = asin( - sin(lat1) * cos(dist / spherical_radius) - + cos(lat1) * sin(dist / spherical_radius) * cos(bearing) - ) - - lon2 = lon1 + atan2( - sin(bearing) * sin(dist / spherical_radius) * cos(lat1), - cos(dist / spherical_radius) - sin(lat1) * sin(lat2), - ) - - return degrees(lat2), degrees(lon2) - - @classmethod - def cross_track_distance( - cls, - origin: LAT_LON | GEO_POINT, - destination: LAT_LON | GEO_POINT, - point: LAT_LON | GEO_POINT, - units: str = "nmi", - ) -> float: - """Find the minimum distance from a point to a great circle path. - - Args: - origin (LAT_LON): Great circle start - destination (LAT_LON): Great circle end - point (LAT_LON): Point to start distance measurement - units (str, optional): Units for the distance output. Defaults to 'nmi'. - - Returns: - float: Distance from point to origin->destination great circle - """ - origin = _convert_geo(origin) - destination = _convert_geo(destination) - point = _convert_geo(point) - delta_1_3 = cls.distance(origin, point, units="m") / spherical_radius - theta_1_3 = radians(cls.bearing(origin, point)) - theta_1_2 = radians(cls.bearing(origin, destination)) - - dist = asin(sin(delta_1_3) * sin(theta_1_3 - theta_1_2)) * spherical_radius - return unit_convert(abs(dist), "m", units) - - @classmethod - def cross_track_point( - cls, - origin: LAT_LON | GEO_POINT, - destination: LAT_LON | GEO_POINT, - point: LAT_LON | GEO_POINT, - ) -> LAT_LON: - """Find the point on a great circle that minimizes the distance to another point. - - Args: - origin (LAT_LON): Great circle start - destination (LAT_LON): Great circle end - point (LAT_LON): Point to start distance measurement - - Returns: - LAT_LON: Location on great circle closest to a point - """ - origin = _convert_geo(origin) - destination = _convert_geo(destination) - point = _convert_geo(point) - delta_1_3 = cls.distance(origin, point, units="m") / spherical_radius - theta_1_3 = radians(cls.bearing(origin, point)) - theta_1_2 = radians(cls.bearing(origin, destination)) - d = asin(sin(delta_1_3) * sin(theta_1_3 - theta_1_2)) - - dist_from_origin = acos(cos(delta_1_3) / cos(d)) * spherical_radius - cross_track_point = cls.point_from_bearing_dist( - origin, - degrees(theta_1_2), - dist_from_origin, - "m", - ) - return cross_track_point - - @classmethod - def ecef_linspace( - cls, - start: LAT_LON | GEO_POINT, - finish: LAT_LON | GEO_POINT, - start_alt: float, - finish_alt: float, - segments: int, - ) -> POSITIONS: - """Get an array of spaced points along a greate circle path in ECEF. - - Args: - start (LAT_LON): Start point (lat/lon degrees) - finish (LAT_LON): End point (lat/lon degrees) - start_alt (float): Starting altitude (meters) - finish_alt (float): Ending altitude (meters) - segments (int): Number of segments to create - - Returns: - POSITIONS: ECEF coordinates - """ - start = _convert_geo(start) - finish = _convert_geo(finish) - lats, lons = cls.geo_linspace(start, finish, num_segments=segments) - delta_alt = finish_alt - start_alt - alts = [start_alt + delta_alt * i / segments for i in range(segments + 1)] - lla = [(lat, lon, alt) for lat, lon, alt in zip(lats, lons, alts)] - return cls.lla2ecef(lla) - - @classmethod - def ecef_and_geo_linspace( - cls, - start: LAT_LON | GEO_POINT, - finish: LAT_LON | GEO_POINT, - start_alt: float, - finish_alt: float, - segments: int, - ) -> tuple[POSITIONS, POSITIONS]: - """Get an array of spaced points along a greate circle path in ECEF and LLA. - - Args: - start (LAT_LON): Start point (lat/lon degrees) - finish (LAT_LON): End point (lat/lon degrees) - start_alt (float): Starting altitude (meters) - finish_alt (float): Ending altitude (meters) - segments (int): Number of segments to create - - Returns: - POSITIONS: ECEF coordinates - POSITIONS: Lat/Lon/Alt (degrees) - """ - start = _convert_geo(start) - finish = _convert_geo(finish) - lats, lons, _ = cls.geo_linspace_with_ecef(start, finish, num_segments=segments) - delta_alt = finish_alt - start_alt - alts = [start_alt + delta_alt * i / segments for i in range(segments + 1)] - lla = [(lat, lon, alt) for lat, lon, alt in zip(lats, lons, alts)] - return cls.lla2ecef(lla), lla diff --git a/src/upstage_des/geography/wgs84.py b/src/upstage_des/geography/wgs84.py deleted file mode 100644 index 662b52c..0000000 --- a/src/upstage_des/geography/wgs84.py +++ /dev/null @@ -1,391 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""WGS84 Earth model.""" - -from math import atan, atan2, cos, degrees, radians, sin, sqrt, tan - -from upstage_des.units import unit_convert - -from .conversions import WGS84_A, WGS84_B, WGS84_F, WGS84Conversions -from .geo_types import GEO_POINT, LAT_LON, POSITIONS, _convert_geo - - -class WGS84(WGS84Conversions): - """Geographical math using elliptical coordinates for Earth. - - Based on Vincenty's methods and WGS84 parameters. - """ - - @classmethod - def distance( - cls, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - units: str = "nmi", - tol: float = 1e-12, - max_iter: int = 200, - ) -> float: - """Find distance between two points. - - Return the great circle distance from the loc1 to another loc2. - - Args: - loc1 (LAT_LON): Starting point (degrees) - loc2 (LAT_LON): Ending point (degrees) - units (str, optional): Units requested for distance. Defaults to 'nmi'. - tol (float, optional): Calculation convergence tolerance. Defaults to 1e-12 - max_iter (int, optional): Max iterations. Defaults to 200 - - Returns: - float: Distances in the defined units - """ - dist, _ = cls.distance_and_bearing(loc1, loc2, units, tol, max_iter) - return dist - - @classmethod - def bearing( - cls, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - tol: float = 1e-12, - max_iter: int = 200, - ) -> float: - """Calculate the forward bearing (in degrees) from loc1 to loc2. - - Args: - loc1 (LAT_LON): Starting point (degrees) - loc2 (LAT_LON): Ending point (degrees) - units (str, optional): Units requested for distance. Defaults to 'nmi'. - tol (float, optional): Calculation convergence tolerance. Defaults to 1e-12 - max_iter (int, optional): Max iterations. Defaults to 200 - - Returns: - float: Bearing - """ - _, angle = cls.distance_and_bearing(loc1, loc2, tol=tol, max_iter=max_iter) - return angle - - @classmethod - def distance_and_bearing( - cls, - loc1: LAT_LON | GEO_POINT, - loc2: LAT_LON | GEO_POINT, - units: str = "nmi", - tol: float = 1e-12, - max_iter: int = 200, - ) -> tuple[float, float]: - """Find great circle distance and forward bearing between two points. - - Args: - loc1 (LAT_LON): Starting point - loc2 (LAT_LON): Ending point - units (str, optional): Distance units requested. Defaults to 'nmi'. - tol (float, optional): Calculation convergence tolerance. Defaults to 1e-12 - max_iter (int, optional): Max iterations. Defaults to 200 - - Returns: - tuple[float, float]: Distance and Bearing - """ - loc1 = _convert_geo(loc1) - loc2 = _convert_geo(loc2) - if loc1[0] == loc2[0] and loc1[1] == loc2[1]: - return 0.0, 0.0 - # reduced latitudes - u1 = atan((1 - WGS84_F) * tan(radians(loc1[0]))) - u2 = atan((1 - WGS84_F) * tan(radians(loc2[0]))) - delta_lon = radians(loc2[1] - loc1[1]) - lambda_lon = delta_lon - - sin_u1 = sin(u1) - cos_u1 = cos(u1) - sin_u2 = sin(u2) - cos_u2 = cos(u2) - - for _ in range(max_iter): - sin_lambda = sin(lambda_lon) - cos_lambda = cos(lambda_lon) - sin_sigma = sqrt( - (cos_u2 * sin_lambda) ** 2 + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda) ** 2 - ) - - cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda - sigma = atan2(sin_sigma, cos_sigma) - sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma - cos_sq_alpha = 1 - sin_alpha**2 - try: - cos2_sigma_m = cos_sigma - 2 * sin_u1 * sin_u2 / cos_sq_alpha - except ZeroDivisionError: - cos2_sigma_m = 0 - c = WGS84_F / 16 * cos_sq_alpha * (4 + WGS84_F * (4 - 3 * cos_sq_alpha)) - _lambda_prev = lambda_lon - lambda_lon = delta_lon + (1 - c) * WGS84_F * sin_alpha * ( - sigma + c * sin_sigma * (cos2_sigma_m + c * cos_sigma * (-1 + 2 * cos2_sigma_m**2)) - ) - if abs(lambda_lon - _lambda_prev) < tol: - break # successful convergence - else: - raise ValueError("Could not converge distance/bearing calculation.") - - u_sq = cos_sq_alpha * (WGS84_A**2 - WGS84_B**2) / (WGS84_B**2) - a = 1 + u_sq / 16384 * (4096 + u_sq * (-768 + u_sq * (320 - 175 * u_sq))) - b = u_sq / 1024 * (256 + u_sq * (-128 + u_sq * (74 - 47 * u_sq))) - delta_sigma = ( - b - * sin_sigma - * ( - cos2_sigma_m - + b - / 4 - * ( - cos_sigma * (-1 + 2 * cos2_sigma_m**2) - - b / 6 * cos2_sigma_m * (-3 + 4 * sin_sigma**2) * (-3 + 4 * cos2_sigma_m**2) - ) - ) - ) - - dist_m = round(WGS84_B * a * (sigma - delta_sigma), 6) - - azimuth = atan2( - cos_u2 * sin_lambda, - cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda, - ) - return unit_convert(dist_m, "m", units), degrees(azimuth) % 360 - - @classmethod - def point_from_bearing_dist( - cls, - point: LAT_LON | GEO_POINT, - bearing: float, - distance: float, - distance_units: str = "nmi", - tol: float = 1e-12, - max_iter: int = 200, - ) -> LAT_LON: - """Get a Location from a starting point, a bearing, and a distance. - - Args: - point (LAT_LON): Starting point (degrees) - bearing (float): Bearing to travel along (degrees) - distance (float): Distance to travel - distance_units (str, optional): Units of the distance. Defaults to 'nmi'. - tol (float, optional): Calculation convergence tolerance. Defaults to 1e-12 - max_iter (int, optional): Max iterations. Defaults to 200 - - Returns: - LAT_LON: The point in degrees - """ - point = _convert_geo(point) - s = unit_convert(distance, distance_units, "km") * 1000 - phi_1 = radians(point[0]) - lambda_1 = radians(point[1]) - a, b, f = WGS84_A, WGS84_B, WGS84_F - alpha_1 = radians(bearing) - - sin_alpha_1 = sin(alpha_1) - cos_alpha_1 = cos(alpha_1) - - tan_u1 = (1 - f) * tan(phi_1) - cos_u1 = 1 / sqrt(1 + tan_u1**2) - sin_u1 = tan_u1 * cos_u1 - - sigma_1 = atan2(tan_u1, cos_alpha_1) - sin_alpha = cos_u1 * sin_alpha_1 - cos_sq_alpha = 1 - sin_alpha**2 - u_sq = cos_sq_alpha * (a**2 - b**2) / (b**2) - A = 1 + u_sq / 16384 * (4096 + u_sq * (-768 + u_sq * (320 - 175 * u_sq))) - B = u_sq / 1024 * (256 + u_sq * (-128 + u_sq * (74 - 47 * u_sq))) - - sigma = s / (b * A) - for _ in range(max_iter): - cos2_sigma_m = cos(2 * sigma_1 + sigma) - sin_sigma = sin(sigma) - cos_sigma = cos(sigma) - delta_sigma = ( - B - * sin_sigma - * ( - cos2_sigma_m - + B - / 4 - * ( - cos_sigma * (-1 + 2 * cos2_sigma_m**2) - - B - / 6 - * cos2_sigma_m - * (-3 + 4 * sin_sigma**2) - * (-3 + 4 * cos2_sigma_m**2) - ) - ) - ) - sigma_prime = sigma - sigma = s / (b * A) + delta_sigma - if abs(sigma_prime - sigma) < tol: - break - else: - raise ValueError("Failed to converge on point from bearing and distance.") - - x = sin_u1 * sin_sigma - cos_u1 * cos_sigma * cos_alpha_1 - phi_2 = atan2( - sin_u1 * cos_sigma + cos_u1 * sin_sigma * cos_alpha_1, - (1 - f) * sqrt(sin_alpha**2 + x**2), - ) - lam = atan2( - sin_sigma * sin_alpha_1, - cos_u1 * cos_sigma - sin_u1 * sin_sigma * cos_alpha_1, - ) - C = f / 16 * cos_sq_alpha * (4 + f * (4 - 3 * cos_sq_alpha)) - L = lam - (1 - C) * f * sin_alpha * ( - sigma + C * sin_alpha * (cos2_sigma_m + C * cos_sigma * (-1 + 2 * cos2_sigma_m**2)) - ) - lambda_2 = lambda_1 + L - - lat, lon = degrees(phi_2), degrees(lambda_2) - return lat, lon - - @classmethod - def geo_linspace( - cls, - start: LAT_LON | GEO_POINT, - end: LAT_LON | GEO_POINT, - num_segments: int = 10, - ) -> list[LAT_LON]: - """Make a discrete great circle path between two points. - - Altitudes are not considered since they do not affect angular coordinates. - - The number of points is the number - - Args: - start (LAT_LON): Start point - end (LAT_LON): End point - num_segments (int, optional): Number of segments on the path. Defaults to 10. - - Returns: - list[LAT_LON]: Lattiude and Longitude (degrees) - """ - start = _convert_geo(start) - end = _convert_geo(end) - if num_segments < 2: - raise ValueError("Not enough points for interpolation") - - # total distance in km - dist, bearing = cls.distance_and_bearing(start, end, units="km") - - lats = [] - lons = [] - for fraction in range(0, num_segments + 1): - f = fraction / num_segments - if fraction == 0: - loc = start - elif fraction == num_segments: - loc = end - else: - loc = cls.point_from_bearing_dist( - start, - bearing, - dist * f, - distance_units="km", - ) - lats.append(loc[0]) - lons.append(loc[1]) - - return [(la, lo) for la, lo in zip(lats, lons)] - - @classmethod - def geo_circle( - cls, - center: LAT_LON | GEO_POINT, - radius: float, - radius_units: str = "nmi", - num_points: int = 10, - ) -> list[LAT_LON]: - """Make an approximate circle by sweeping bearing from a central location. - - Altitudes are not considered since they do not affect angular coordinates. - - Args: - center (LAT_LON): Center point (degrees) - radius (float): Radius of the circle - radius_units (str, optional): Units of the radius. Defaults to 'nmi'. - num_points (int, optional): Number of points on the circle. Defaults to 10. - - Returns: - list[LAT_LON]: Latitude and Longitude (degrees) of the circle - """ - center = _convert_geo(center) - lats = [] - lons = [] - for fraction in range(0, num_points + 1): - f = fraction / num_points - bearing = f * 360 - loc = cls.point_from_bearing_dist( - center, - bearing, - radius, - distance_units=radius_units, - ) - lats.append(loc[0]) - lons.append(loc[1]) - - return [(la, lo) for la, lo in zip(lats, lons)] - - @classmethod - def ecef_linspace( - cls, - start: LAT_LON | GEO_POINT, - finish: LAT_LON | GEO_POINT, - start_alt: float, - finish_alt: float, - segments: int, - ) -> POSITIONS: - """Get an array of spaced points along a greate circle path in ECEF. - - Args: - start (LAT_LON): Start point (lat/lon degrees) - finish (LAT_LON): End point (lat/lon degrees) - start_alt (float): Starting altitude (meters) - finish_alt (float): Ending altitude (meters) - segments (int): Number of segments to create - - Returns: - POSITIONS: ECEF coordinates - """ - start = _convert_geo(start) - finish = _convert_geo(finish) - lats, lons = cls.geo_linspace(start, finish, num_segments=segments) - delta_alt = finish_alt - start_alt - alts = [start_alt + delta_alt * i / segments for i in range(segments + 1)] - lla = [(lat, lon, alt) for lat, lon, alt in zip(lats, lons, alts)] - return cls.lla2ecef(lla) - - @classmethod - def ecef_and_geo_linspace( - cls, - start: LAT_LON | GEO_POINT, - finish: LAT_LON | GEO_POINT, - start_alt: float, - finish_alt: float, - segments: int, - ) -> tuple[POSITIONS, POSITIONS]: - """Get an array of spaced points along a greate circle path in ECEF and LLA. - - Args: - start (LAT_LON): Start point (lat/lon degrees) - finish (LAT_LON): End point (lat/lon degrees) - start_alt (float): Starting altitude (meters) - finish_alt (float): Ending altitude (meters) - segments (int): Number of segments to create - - Returns: - POSITIONS: ECEF coordinates - list[tuple[float, float, float]]: Lat/Lon/Alt (degrees) - """ - start = _convert_geo(start) - finish = _convert_geo(finish) - latlons = cls.geo_linspace(start, finish, num_segments=segments) - delta_alt = finish_alt - start_alt - alts = [start_alt + delta_alt * i / segments for i in range(segments + 1)] - lla: POSITIONS = [(latlon[0], latlon[1], alt) for latlon, alt in zip(latlons, alts)] - return cls.lla2ecef(lla), lla diff --git a/src/upstage_des/math_utils.py b/src/upstage_des/math_utils.py deleted file mode 100644 index baa6157..0000000 --- a/src/upstage_des/math_utils.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""This module contains math utility functions to avoid numpy.""" - -from math import sqrt - -VECTOR = list[float] | tuple[float, ...] - - -def _vector_subtract(vector_a: VECTOR, vector_b: VECTOR) -> list[float]: - """Subtract equal-sized vectors. - - Args: - vector_a (list[float]): Left vector - vector_b (list[float]): Right vector - - Returns: - list[float]: Subtracted vector - """ - if not (len(vector_a) == len(vector_b)): - raise ValueError("Vectors are not the same size") - - ret = [a - b for a, b in zip(vector_a, vector_b)] - return ret - - -def _vector_add(vector_a: VECTOR, vector_b: VECTOR) -> list[float]: - """Add equal-sized vectors. - - Args: - vector_a (list[float]): Left vector - vector_b (list[float]): Right vector - - Returns: - list[float]: Added vector - """ - if not (len(vector_a) == len(vector_b)): - raise ValueError("Vectors are not the same size") - - ret = [a + b for a, b in zip(vector_a, vector_b)] - return ret - - -def _vector_dot(vector_a: VECTOR, vector_b: VECTOR) -> float: - """Inner product of two vectors. - - Args: - vector_a (VECTOR): Left vector - vector_b (VECTOR): Right vector - - Returns: - float: inner product - """ - return sum(a * b for a, b in zip(vector_a, vector_b)) - - -def _vector_norm(arr: VECTOR) -> float: - """Norm of a vector. - - Args: - arr (VECTOR): vector - - Returns: - float: norm - """ - s = sum(a**2 for a in arr) - return sqrt(s) - - -def _roots(a: float, b: float, c: float) -> list[float]: - """Calculate the roots of a quadratic. - - The form is ax^2 + bx + c = 0. - - Args: - a (float): Coefficient on the square term - b (float): Coefficient on the base term - c (float): Constant - - Returns: - list[float]: The two roots, empty if not real. - """ - discriminant = b**2 - 4 * a * c - if discriminant < 0: - return [] - root1 = (-b + sqrt(discriminant)) / (2 * a) - root2 = (-b - sqrt(discriminant)) / (2 * a) - return [root1, root2] - - -def _col_mat_mul(col: VECTOR, mat: list[list[float]]) -> list[float]: - """Do a matrix multiplication of a column against a matrix. - - Does col @ mat - - Args: - col (VECTOR): The left vector - mat (list[list[float]]): The matrix - - Returns: - list[float]: The multiplied result - """ - if len(col) != len(mat): - raise ValueError("Number of values in col must equal number of rows in mat") - - result = [sum(x * y for x, y in zip(col, c)) for c in zip(*mat)] - - return result diff --git a/src/upstage_des/motion/__init__.py b/src/upstage_des/motion/__init__.py deleted file mode 100644 index a74cc6b..0000000 --- a/src/upstage_des/motion/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Motion features for UPSTAGE.""" - -from .motion import MotionAndDetectionError, SensorMotionManager -from .stepped_motion import SteppedMotionManager - -__all__ = ["MotionAndDetectionError", "SensorMotionManager", "SteppedMotionManager"] diff --git a/src/upstage_des/motion/cartesian_model.py b/src/upstage_des/motion/cartesian_model.py deleted file mode 100644 index a58c5d7..0000000 --- a/src/upstage_des/motion/cartesian_model.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""0A 3-D intersection model for cartesian motion in UPSTAGE along straight-line paths.""" - -from typing import TypeVar - -from upstage_des.base import MotionAndDetectionError -from upstage_des.data_types import CartesianLocation -from upstage_des.math_utils import ( - _col_mat_mul, - _roots, - _vector_add, - _vector_dot, - _vector_norm, - _vector_subtract, -) - -XY = tuple[float, float] -XYZ = tuple[float, float, float] - -L = TypeVar("L", XY, XYZ) - - -def ray_intersection( - start: L, - toward: L, - center: L, - radii: float | L, - speed: float, -) -> tuple[list[L], list[float]]: - """Ray intersection with ellispoid. - - Args: - start (XY | XYZ): Start position - toward (XY | XYZ): Point being looked at - center (XY | XYZ): Center of the ellipsoid - radii (float | XY | XYZ): Radii of each dimension of ellipsoid - speed (float): Speed of the particle moving from start to toward - - Returns: - tuple[list[XY] | list[XYZ], list[float]]: Intersecting positions and times. - """ - n_dim = len(start) - for compare in [toward, center]: - assert len(compare) == n_dim, "Mismatched dimensions in ray intersection." - - M = [[0.0 for _ in range(n_dim)] for _ in range(n_dim)] - _radii: tuple[float, ...] - if isinstance(radii, float | int): - _radii = tuple([float(radii)] * n_dim) - elif len(radii) == 1: - _radii = tuple([radii[0]] * n_dim) - else: - _radii = radii - if len(start) != len(radii): - raise MotionAndDetectionError( - "Radius for a sensor must be a single float or the same dimensionality as the " - "locations." - ) - for i in range(n_dim): - M[i][i] = 1 / _radii[i] - - v = _vector_subtract(toward, start) - v1 = _col_mat_mul(v, M) - vec1 = _col_mat_mul(start, M) - vec2 = _col_mat_mul(center, M) - P1 = _vector_subtract(vec1, vec2) - c = _vector_norm(P1) ** 2 - 1 - b = 2 * _vector_dot(P1, v1) - a = _vector_norm(v1) ** 2 - roots = _roots(a, b, c) - possible = [r for r in roots if r >= 0] - - intersections: list[L] = [] - times: list[float] = [] - for t in sorted(possible): - loc = _vector_add(start, [t * x for x in v]) - dist = float(_vector_norm(_vector_subtract(loc, start))) - times.append(dist / speed) - intersections.append(tuple(loc)) # type: ignore [arg-type] - return intersections, times - - -def cartesian_linear_intersection( - start: CartesianLocation, - finish: CartesianLocation, - speed: float, - sensor_location: CartesianLocation, - sensor_radius: float, -) -> tuple[list[CartesianLocation], list[float], list[str], float]: - """Get the intersection of straight line motion and a sphere. - - Args: - start (CartesianLocation): the start location of the mover - finish (CartesianLocation): the finish location of the mover - speed (float): the speed of the mover (in STAGE units) - sensor_location (CartesianLocation): the location of the sensor - sensor_radius (float): the radius of the sensor (in STAGE units) - - Returns: - tuple[list[CartesianLocation], list[float], list[str], float]: _description_ - """ - path_dist = finish - start - path_time = path_dist / speed - dist_start = sensor_location - start - dist_finish = sensor_location - finish - start_tup = (start.x, start.y, start.z) - finish_tup = (finish.x, finish.y, finish.z) - - # for start and finish inside, we can skip the intersection math - start_inside = dist_start <= sensor_radius - finish_inside = dist_finish <= sensor_radius - if start_inside and finish_inside: - inters = [start, finish] - the_times = [0, path_time] - return inters, the_times, ["START_INSIDE", "END_INSIDE"], path_time - - sensor_location_tup = (sensor_location.x, sensor_location.y, sensor_location.z) - intersections, times = ray_intersection( - start_tup, - finish_tup, - sensor_location_tup, - sensor_radius, - speed, - ) - if not intersections or min(times) > path_time: - return [], [], ["BAD", "BAD"], -1.0 - - # filter out intersections beyond the path length - idxs = sorted(range(len(times)), key=lambda i: times[i]) - inters = [CartesianLocation(*intersections[i]) for i in idxs if times[i] <= path_time] - times = [times[i] for i in idxs if times[i] <= path_time] - type_start = "START_INSIDE" if start_inside else "ENTER" - type_end = "END_INSIDE" if finish_inside else "EXIT" - if start_inside: - inters = [start] + inters - times = [0] + times - if finish_inside: - inters = inters + [finish] - times = times + [path_time] - if len(inters) != 2: - raise MotionAndDetectionError( - f"There should be two intersections, not {len(inters)} ({inters})" - ) - if len(times) != 2: - raise MotionAndDetectionError("There should be two time intersections") - - return inters, times, [type_start, type_end], path_time diff --git a/src/upstage_des/motion/geodetic_model.py b/src/upstage_des/motion/geodetic_model.py deleted file mode 100644 index a01ab3d..0000000 --- a/src/upstage_des/motion/geodetic_model.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""A model of the geodetic earth and intersecting paths/spheres.""" - -from math import sqrt - -from upstage_des.data_types import GeodeticLocation -from upstage_des.geography import ( - INTERSECTION_LOCATION_CALLABLE, - CrossingCondition, - Spherical, -) -from upstage_des.motion.great_circle_calcs import get_dist_rad, get_great_circle_points -from upstage_des.units import unit_convert - - -def _to_tuple(loc: GeodeticLocation) -> tuple[float, float, float]: - """Convert a Geodetic location to a tuple. - - Properly corrects for meters being expected. - - Args: - loc (GeodeticLocation): Lat/Lon/Alt location - - Returns: - tuple[float, float, float]: Data as tuple - """ - _l = loc.to_degrees() - units: str = loc.stage.altitude_units - alt = unit_convert(_l.alt, units, "m") - return (_l.lat, _l.lon, alt) - - -def subdivide_intersection( - start: GeodeticLocation, - finish: GeodeticLocation, - speed: float, - sensor_location: GeodeticLocation, - sensor_radius: float, -) -> tuple[list[GeodeticLocation], list[float], list[str], float]: - """Numerical intersection calculation. - - Requires: - UP.add_stage_variable("intersection_model", INTERSECTION_LOCATION_CALLABLE) - UP.add_stage_variable("distance_units", ...) - - Args: - start (GeodeticLocation): Path start - finish (GeodeticLocation): Path end - speed (float): Speed on path - sensor_location (GeodeticLocation): Location of a sensor - sensor_radius (float): Radius of sensor line of sight. - - Returns: - tuple[list[GeodeticLocation], list[float], list[str], float]: intersections, - times, types, path_time - """ - STAGE = start.stage - alt_units: str = STAGE.altitude_units - dist_units: str = STAGE.distance_units - intersection: INTERSECTION_LOCATION_CALLABLE = STAGE.intersection_model - path_dist = finish - start - path_time = path_dist / speed - intersect_locs = intersection( - _to_tuple(start), - _to_tuple(finish), - _to_tuple(sensor_location), - sensor_radius, - dist_units, - STAGE.stage_model, - 9260, - [10, 20], - ) - - if not intersect_locs: - return [], [], ["Bad", "Bad"], 0.0 - - # convert the data to a more useful format - intersections: list[GeodeticLocation] = [] - times: list[float] = [] - types: list[str] = [] - condition: CrossingCondition - for condition in intersect_locs: - lat, lon, alt = condition.begin - alt = unit_convert(alt, "m", alt_units) - the_loc = GeodeticLocation(lat, lon, alt) - dist_from_start = the_loc - start - time_from_start = dist_from_start / speed - intersections.append(the_loc) - times.append(time_from_start) - types.append(condition.kind) - - return intersections, times, types, path_time - - -def analytical_intersection( - start: GeodeticLocation, - finish: GeodeticLocation, - speed: float, - sensor_location: GeodeticLocation, - sensor_radius: float, -) -> tuple[list[GeodeticLocation], list[float], list[str], float]: - """Calculate the intersection of a great circle. - - The circle is defined by start & finish and the sphere defined by - sensor_location and sensor_radius. - - This function mimics the above `subdivide_intersection`, but uses analytical - equations that run much faster. - - Requires: - UP.add_stage_variable("distance_units", ...) - UP.add_stage_variable("altitude_units", ...) - - Args: - start (GeodeticLocation): the start location of the mover - finish (GeodeticLocation): the finish location of the mover - speed (float): the speed of the mover (in STAGE units) - sensor_location (GeodeticLocation): the location of the sensor - sensor_radius (float): the radius of the sensor (in STAGE units) - - Returns: - tuple[list[GeodeticLocation], list[float], list[str], float]: - intersections, times, types, path_time - """ - STAGE = start.stage - dist_units: str = STAGE.distance_units - altitude_units: str = STAGE.altitude_units - # convert some units - earth_rad = unit_convert(Spherical.EARTH_RADIUS, "m", dist_units) - start_rad = start.to_radians() - finish_rad = finish.to_radians() - sensor_location_rad = sensor_location.to_radians() - - # modify the sensor's radius to account for the altitude of the mover - average_path_height = (start.alt + finish.alt) / 2.0 - average_path_height_dist_units = unit_convert(average_path_height, altitude_units, dist_units) - adjusted_sensor_radius = sqrt(sensor_radius**2 - average_path_height_dist_units**2) - sensor_radius_rad = adjusted_sensor_radius / earth_rad - - # Note: some calcs may be doubled here, further speed up potentially by optimizing this - point_results = get_great_circle_points( - start_rad, finish_rad, sensor_location_rad, sensor_radius_rad - ) - dist_start = get_dist_rad(sensor_location_rad, start_rad) - dist_finish = get_dist_rad(sensor_location_rad, finish_rad) - path_dist = get_dist_rad(start_rad, finish_rad) - path_time = (path_dist * earth_rad) / speed - - # no intersection - if point_results is None: - # return matches subdivide_intersection results - return [], [], ["Bad", "Bad"], -1.0 - - points, distances = point_results - # get points, times, and types - intersections = [] - times = [] - types = [] - alt_change_per_dist = (finish.alt - start.alt) / path_dist # altitude change per radian - - # check for start in the sensor range - if dist_start < sensor_radius_rad: - types.append("START_INSIDE") - intersections.append(start) - times.append(0.0) - else: - types.append("ENTER") - - # estimate intersection altitude assuming linear change - alt_1 = start.alt + (distances[0] / path_dist) * alt_change_per_dist - # adjust distance to account for average altitude from start to first intersection - d1_m = distances[0] * ( - earth_rad + unit_convert(0.5 * (start.alt + alt_1), altitude_units, dist_units) - ) - - intersections.append( - GeodeticLocation( - *points[0], - alt=alt_1, - in_radians=True, - ).to_degrees() - ) - times.append(d1_m / speed) - - # check for end in the sensor range - if dist_finish < sensor_radius_rad: - types.append("END_INSIDE") - intersections.append(finish) - d_end = path_dist * earth_rad - times.append(d_end / speed) - else: - types.append("EXIT") - - # estimate intersection altitude assuming linear change - alt_2 = start.alt + (distances[1] / path_dist) * alt_change_per_dist - # adjust distance to account for average altitude from start to first intersection - d2_m = distances[1] * ( - earth_rad + unit_convert(0.5 * (start.alt + alt_2), altitude_units, dist_units) - ) - - intersections.append(GeodeticLocation(*points[1], alt=alt_2, in_radians=True).to_degrees()) - times.append(d2_m / speed) - - return intersections, times, types, path_time diff --git a/src/upstage_des/motion/great_circle_calcs.py b/src/upstage_des/motion/great_circle_calcs.py deleted file mode 100644 index ac940e2..0000000 --- a/src/upstage_des/motion/great_circle_calcs.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Great circle calculations. - -These equations were largely adapted from https://edwilliams.org/avform147.htm, -although most of them can be verified from a number of open resources around -the web. The overall algorithms have been slightly modified to support UPSTAGE. -""" - -from functools import lru_cache -from math import acos, asin, atan2, cos, pi, sin, sqrt -from typing import cast - -from upstage_des.data_types import GeodeticLocation - - -@lru_cache -def get_dist_rad(point1: GeodeticLocation, point2: GeodeticLocation) -> float: - """Get the distance (in radians) between point1 and point2. - - :param point1: the starting point - :param point2: the ending point - """ - point1 = point1.to_radians() - point2 = point2.to_radians() - ans = 2.0 * asin( - sqrt( - (sin((point1.lat - point2.lat) / 2.0)) ** 2 - + cos(point1.lat) * cos(point2.lat) * (sin((point1.lon - point2.lon) / 2.0)) ** 2 - ) - ) - return cast(float, ans) - - -@lru_cache -def get_course_rad(point1: GeodeticLocation, point2: GeodeticLocation) -> float: - """Get the course (in radians) between point1 and point2. - - :param point1: the starting point - :param point2: the ending point - """ - point1 = point1.to_radians() - point2 = point2.to_radians() - tcl: float - - d = get_dist_rad(point1, point2) - - if sin(point2.lon - point1.lon) < 0: - tcl = acos((sin(point2.lat) - sin(point1.lat) * cos(d)) / (sin(d) * cos(point1.lat))) - else: - tcl = 2.0 * pi - acos( - (sin(point2.lat) - sin(point1.lat) * cos(d)) / (sin(d) * cos(point1.lat)) - ) - - return tcl - - -@lru_cache -def get_pos_from_points_and_distance( - point1: GeodeticLocation, point2: GeodeticLocation, dist: float -) -> tuple[float, float]: - """Get a position (lat, lon) given a starting position, ending position, and distance. - - :param point1: GeodeticLocation of start of great circle - :param point2: GeodeticLocation of end of great circle - :param dist: (float) distance along great circle to find third point - - returns [lat, lon] - """ - point1 = point1.to_radians() - point2 = point2.to_radians() - tc = get_course_rad(point1, point2) # course from point 1 to 2 - - lat: float = asin(sin(point1.lat) * cos(dist) + cos(point1.lat) * sin(dist) * cos(tc)) - - dlon = atan2( - sin(tc) * sin(dist) * cos(point1.lat), - cos(dist) - sin(point1.lat) * sin(lat), - ) - - lon: float = ((point1.lon - dlon + pi) % (2.0 * pi)) - pi - - return (lat, lon) - - -@lru_cache -def get_great_circle_points( - point_a: GeodeticLocation, - point_b: GeodeticLocation, - point_d: GeodeticLocation, - dist: float, -) -> tuple[list[tuple[float, float]], list[float]] | None: - """Let points A and B define a great circle route and D be a third point. - - Find the points on the great circle through A and B that lie a distance d from D, if they exist. - - :param point_a: GeodeticLocation of start of great circle - :param point_b: GeodeticLocation of end of great circle - :param point_d: GeodeticLocation, third point of interest (the center of sphere) - :param dist: (float) distance from third to point to find intersection on great circle (radians) - """ - point_a = point_a.to_radians() - point_b = point_b.to_radians() - point_d = point_d.to_radians() - course_ad = get_course_rad(point_a, point_d) - course_ab = get_course_rad(point_a, point_b) - - a = course_ad - course_ab - b = get_dist_rad(point_a, point_d) - - r = (cos(b) ** 2 + sin(b) ** 2 * cos(a) ** 2) ** ( - 1 / 2 - ) # arccos(r) is the cross track distance - - atd = atan2(sin(b) * cos(a), cos(b)) # the along track distance - - dist_ab = get_dist_rad(point_a, point_b) - - if cos(dist) ** 2 > r**2: - # no points exist - dp = None - else: - # two points exist - dp = acos(cos(dist) / r) - - if dp: - d1 = atd - dp - d2 = atd + dp - - # make sure we can get to first crossing - # if second cross is negative, both points are outside the D/dist radius - if dist_ab < d1 or d2 < 0: - return None - - p1 = get_pos_from_points_and_distance(point_a, point_b, d1) - p2 = get_pos_from_points_and_distance(point_a, point_b, d2) - - else: - return None - - return [p1, p2], [d1, d2] diff --git a/src/upstage_des/motion/motion.py b/src/upstage_des/motion/motion.py deleted file mode 100644 index 3d670df..0000000 --- a/src/upstage_des/motion/motion.py +++ /dev/null @@ -1,493 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""This file contains a queueing motion manager for sensor/mover intersections.""" - -from collections.abc import Callable, Generator -from typing import Any, Protocol, TypeVar -from warnings import warn - -from simpy import Event as SimpyEvent -from simpy import Interrupt, Process - -from upstage_des.actor import Actor -from upstage_des.base import ( - MotionAndDetectionError, - SimulationError, - UpstageBase, -) -from upstage_des.data_types import CartesianLocation, GeodeticLocation -from upstage_des.states import CartesianLocationChangingState, GeodeticLocationChangingState - -VALID = [ - ("ENTER", "END_INSIDE"), - ("ENTER", "EXIT"), - ("START_INSIDE", "END_INSIDE"), - ("START_INSIDE", "EXIT"), -] - -LOC_TYPES = CartesianLocation | GeodeticLocation -LOC_LIST = list[CartesianLocation] | list[GeodeticLocation] - -LOC_INPUT = TypeVar("LOC_INPUT", "GeodeticLocation", "CartesianLocation") - -INTERSECTION_TIMING_CALLABLE = Callable[ - [ - LOC_INPUT, - LOC_INPUT, - float, - LOC_INPUT, - float, - ], - tuple[ - list[LOC_INPUT], - list[float], - list[str], - float, - ], -] - - -class SensorType(Protocol): - """Protocol class for sensor typing.""" - - def entity_exited_range( - self, - entity: Any, - ) -> None: - """Entity exit range and does something.""" - - def entity_entered_range( - self, - entity: Any, - ) -> None: - """Entity enters range and does something.""" - - -class SensorMotionManager(UpstageBase): - """Schedules the interaction of moving and detectable entities against non-moving 'sensors'. - - Movable objects must be Actors with: - 1. GeodeticLocationChangingState OR CartesianLocationChangingState - 2. DetectabilityState - - Sensor objects MUST implement these two methods: - 1. `entity_entered_range(mover)` - 2. `entity_exited_range(mover)` - - The first is called when a mover enters the sensor's visiblity. - The second is called when a mover leaves the visibility or becomes undetectable. - - The motion manager will learn about sensor objects with: - - sensor_motion_manager.add_sensor(sensor_object, location, radius) - - Where location is a location object found in upstage.data_types and radius - is a distance in the units defined in upstage.STAGE. - - """ - - def __init__( - self, intersection_model: INTERSECTION_TIMING_CALLABLE, debug: bool = False - ) -> None: - """Create a sensor motion manager for queueing intersection events. - - Args: - intersection_model (INTERSECTION_TIMING_CALLABLE): The odel to calculate - intersections. - debug (bool, optional): Allow debug logging to _debug_log. Defaults to False. - """ - super().__init__() - self._sensors: dict[SensorType, tuple[str, str]] = {} - self._movers: dict[Actor, tuple[float, LOC_LIST, float]] = {} - self._events: dict[Actor, list[tuple[SensorType, Process]]] = {} - self._in_view: dict[Actor, set[SensorType]] = {} - self._debug: bool = debug - self._debug_data: dict[Actor, list[Any]] = {} - self._debug_log: list[Any] = [] - self.intersection = intersection_model - - def _test_detect(self, mover: Actor) -> str | None: - detect_state = mover._get_detection_state() - return detect_state - - def _stop_mover(self, mover: Actor, from_not_detectable: bool = False) -> None: - """Stop a mover. - - Args: - mover (Actor): The moving actor. - from_not_detectable (bool, optional): Is this was called from detectability state. - Defaults to False. - """ - detect_state = self._test_detect(mover) - if detect_state is None: - return None - - detectable: bool = getattr(mover, detect_state) - if mover not in self._movers and not detectable: - return None - - # Call this when a mover stops its motion - if mover not in self._movers and not from_not_detectable: - raise MotionAndDetectionError(f"Mover {mover} wasn't moving yet") - elif mover not in self._movers: - return None - # It's possible for a mover to have no events when it stops - # since it has no intersections. But we need the mover to exist - # in case a new sensor pops up - if mover in self._events: - for _, proc in self._events.get(mover, []): - if proc.is_alive: - proc.interrupt() - del self._events[mover] - - # clear the mover references - del self._movers[mover] - return None - - def _mover_not_detectable(self, mover: Actor) -> None: - """Called via DetectabilityState when a mover becomes undectable. - - Could be called for any reason; use this feature to alert sensors that - a mover should no longer be considered by that sensor. - - Args: - mover (Actor): The mover. - """ - if mover in self._in_view: - for sensor in self._in_view[mover]: - # This will cause some old data to stick around, but that's - # instead of making new events to clear it out and then having - # to end those clearing events if this happens - sensor.entity_exited_range(mover) - del self._in_view[mover] - # It is unsure if the user will stop the motion via a task first - # or change detectability first - self._stop_mover(mover, from_not_detectable=True) - - def _mover_became_detectable(self, mover: Actor) -> None: - """Called via DetectabilityState when a mover becomes detectable. - - The actor calls this in its movement states. - - Args: - mover (Actor): The mover. - """ - # Before a mover is 'restarted' when becoming detectable, - # we have to know if it's still moving - move_states = ( - GeodeticLocationChangingState, - CartesianLocationChangingState, - ) - # find if there is a location changing state that is active - locations = [ - name - for name in mover._active_states - if isinstance(mover._state_defs[name], move_states) - ] - if locations: - msg = ( - "Setting DetectabilityState to True while " - "locations states are active won't affect the" - "SensorMotionManager." - ) - warn(msg, UserWarning) - - # TODO: remove sensor or 'not active'? - - def _process_mover_sensor_pair( - self, mover: Actor, sensor: SensorType - ) -> list[tuple[tuple[str, float, LOC_TYPES], tuple[str, float, LOC_TYPES]]]: - """Find the intersections (if any) b/w mover and sensor. - - Args: - mover (Actor): The mover - sensor (SensorType): The sensor - - Returns: - list[tuple[str, float]]: What the movement events are are and their times - """ - # Get pairs of "Inside/entering - Leaving/staying" to send - # to the probability model and scheduler - speed, waypoints, start_time = self._movers[mover] - location_name, radius_name = self._sensors[sensor] - location: LOC_TYPES = getattr(sensor, location_name) - radius: float = getattr(sensor, radius_name) - - # Since waypoints connect, don't keep 'finish_in' unless - # it's the last point in the series - elapsed_time: float = 0.0 - inter_data: list[tuple[str, float, LOC_TYPES]] = [] - for i in range(len(waypoints) - 1): - start, finish = waypoints[i : i + 2] - # These times are relative to the start of the path - intersections, times, types, path_time = self.intersection( - start, - finish, - speed, - location, - radius, - ) - - for ( - inter, - t, - typ, - ) in zip(intersections, times, types): - if inter_data and inter_data[-1][0] == "END_INSIDE": - # drop that one since we are continuing on from that point - # and ignore this current one since it's inside already - if typ != "START_INSIDE": - raise SimulationError("START_INSIDE must follow END_INSIDE") - - inter_data.pop() - continue - inter_data.append((typ, start_time + elapsed_time + t, inter)) - elapsed_time += path_time - - # pair off the in/out - if len(inter_data) % 2 != 0: - raise SimulationError(f"Intersections should pair in/out or in/in, found: {inter_data}") - pairs = [(inter_data[i], inter_data[i + 1]) for i in range(0, len(inter_data), 2)] - if not all((a[0], b[0]) in VALID for a, b in pairs): - raise SimulationError(f"Bad pairing of intersections: {pairs}") - return pairs - - def _add_to_view(self, mover: Actor, sensor: SensorType) -> bool: - """Add a mover to a sensor's view. - - Args: - mover (Actor): Mover - sensor (Actor): Sensor - - Returns: - bool: If it was already in view. - """ - if mover not in self._in_view: - self._in_view[mover] = set() - was_in = sensor in self._in_view[mover] - self._in_view[mover].add(sensor) - return was_in - - def _remove_from_view(self, mover: Actor, sensor: SensorType) -> None: - """Remove a mover from a sensor's view. - - Args: - mover (Actor): Mover - sensor (Actor): Sensor - """ - if mover not in self._in_view: - raise MotionAndDetectionError(f"{mover} isn't in view of anything to remove.") - if sensor not in self._in_view[mover]: - raise MotionAndDetectionError(f"{mover} isn't in view of {sensor} to allow clearing.") - self._in_view[mover].remove(sensor) - - def _end_notify(self, mover: Actor, sensor: SensorType, event: str) -> None: - """End notification and give a reason. - - Args: - mover (Actor): Mover - sensor (Actor): Sensor - event (str): Reason - """ - if self._debug: - msg = { - "time": self.env.now, - "event": f"Detection of a mover cancelled {event}", - "mover": mover, - "sensor": sensor, - } - self._debug_log.append(msg) - - def _notify( - self, - mover: Actor, - sensor: SensorType, - first_time: float, - second_time: float, - first_kind: str, - second_kind: str, - ) -> Generator[SimpyEvent, Any, None]: - """Notify a sensor about a mover. - - Handles entry to exit in one go, making interrupts easier. - - Args: - mover (Actor): Mover - sensor (Actor): Sensor - first_time (float): Time of the first notification - second_time (float): Time of the second - first_kind (str): Kind of the first - second_kind (str): Kind of the second - """ - # times are absolute on input to this method - notify_time_from_now = first_time - self.env.now - - if first_kind == "START_INSIDE" or notify_time_from_now <= 0: - was_in = self._add_to_view(mover, sensor) - if not was_in: - sensor.entity_entered_range(mover) - else: - assert first_kind == "ENTER" - try: - yield self.env.timeout(notify_time_from_now) - except Interrupt: - self._end_notify(mover, sensor, "before entry") - return None - - sensor.entity_entered_range(mover) - self._add_to_view(mover, sensor) - - if second_kind != "EXIT": - return None - - end_time_from_now = second_time - self.env.now - if end_time_from_now <= 0: - raise MotionAndDetectionError("Detection end time is less than detection start") - - try: - yield self.env.timeout(end_time_from_now) - except Interrupt: - self._end_notify(mover, sensor, "before exit") - return None - - sensor.entity_exited_range(mover) - self._remove_from_view(mover, sensor) - return None - - def _schedule( - self, - mover: Actor, - sensor: SensorType, - events: tuple[tuple[str, float, LOC_TYPES], tuple[str, float, LOC_TYPES]], - ) -> None: - """Schedule events based on entry/exit into sensor range. - - Args: - mover (Actor): The mover - sensor (SensorType): The sensor - events (list[tuple[str, float]]): Crossing events (ENTER and EXIT) - """ - if not events: - return - first_kind, first_time, first_loc = events[0] - second_kind: str = "" - second_time: float = first_time - second_loc: LOC_TYPES | None = None - if len(events) > 1: - second_kind, second_time, second_loc = events[1] - - # If both times are in the past, then the segment has already occurred - # and we can skip it. - if first_time <= self.env.now and second_time < self.env.now: - return - - if self._debug: - self._debug_data[mover].append( - ( - sensor, - [first_kind, second_kind], - [first_time, second_time], - [first_loc, second_loc], - ) - ) - msg = { - "time": self.env.now, - "event": "Scheduling sensor detecting mover", - "mover": mover, - "sensor": sensor, - } - self._debug_log.append(msg) - - proc = self.env.process( - self._notify(mover, sensor, first_time, second_time, first_kind, second_kind) - ) - if mover not in self._events: - self._events[mover] = [ - (sensor, proc), - ] - else: - self._events[mover].append((sensor, proc)) - - def _find_intersections( - self, - mover_list: list[Actor] | None = None, - sensor_list: list[SensorType] | None = None, - ) -> None: - """Find all paired intersections and schedule them. - - Optionally, use a reduced list of either movers or sensors. - - Args: - mover_list (list[Actor] | None, optional): Movers to consider. - Defaults to None (all movers). - sensor_list (list[SensorType] | None, optional): Sensors to consider. - Defaults to None (all sensors). - """ - movers = list(self._movers.keys()) if mover_list is None else mover_list - sensors = list(self._sensors.keys()) if sensor_list is None else sensor_list - for m in movers: - for s in sensors: - inter_pairs = self._process_mover_sensor_pair(m, s) - for pair in inter_pairs: - self._schedule(m, s, pair) - - def _start_mover(self, mover: Actor, speed: float, waypoints: LOC_LIST) -> None: - """Start a mover's motion and find intersections with sensors. - - Args: - mover (Actor): The mover - speed (float): Speed (in model units) - waypoints (LOC_LIST): Waypoint of travel. - """ - detect_state = self._test_detect(mover) - if detect_state is None: - return - - detectable = getattr(mover, detect_state) - # Since this class examines self._movers when mover stops, we need to put in - # some data about that so we get the right errors if new motion starts - # when this motion hasn't ended. - if not detectable: - self._movers[mover] = (0.0, [], 0.0) - return None - - if mover in self._movers: - raise MotionAndDetectionError( - f"Mover: {mover} is already known to be on a path. Did you forget to stop it?" - ) - - if self._debug and mover not in self._debug_data: - self._debug_data[mover] = [] - # TODO: Waypoints need to start with the movers current location - self._movers[mover] = (speed, waypoints, self.env.now) - self._find_intersections(mover_list=[mover], sensor_list=None) - return None - - def add_sensor( - self, - sensor: SensorType, - location_attr_name: str = "location", - radius_attr_name: str = "radius", - ) -> None: - """Add a sensor to the motion manager. - - Args: - sensor (SensorType): The sensor object - location_attr_name (str, optional): Name of the location attribute. - Defaults to "location". - radius_attr_name (str, optional): Name of the radius attribute. Defaults to "radius". - """ - # test the sensor for earlier errors about improperly-defined methods - required_methods = ["entity_entered_range", "entity_exited_range"] - for req in required_methods: - if not hasattr(sensor, req): - raise NotImplementedError(f"Sensor {sensor} does not have '{req}' method!") - for attr in [location_attr_name, radius_attr_name]: - _attr = getattr(sensor, attr, None) - if _attr is None: - raise SimulationError(f"Sensor {sensor} has no attribute: {attr}") - - self._sensors[sensor] = (location_attr_name, radius_attr_name) - self._find_intersections(mover_list=None, sensor_list=[sensor]) diff --git a/src/upstage_des/motion/stepped_motion.py b/src/upstage_des/motion/stepped_motion.py deleted file mode 100644 index 150e394..0000000 --- a/src/upstage_des/motion/stepped_motion.py +++ /dev/null @@ -1,320 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""This file contains a motion manager that does time-stepping.""" - -from collections.abc import Callable, Generator -from typing import Any, cast - -from simpy import Event as SimpyEvent - -from upstage_des.actor import Actor -from upstage_des.base import SimulationError, UpstageBase -from upstage_des.motion.motion import LOC_TYPES, SensorType -from upstage_des.states import CartesianLocationChangingState, GeodeticLocationChangingState -from upstage_des.task import process - - -class SteppedMotionManager(UpstageBase): - """Tests relative distances of objects with a location property. - - Reports to "sensor" objects when something enters or exits a range. - - Use this manager when the sensing entities are not static. If they are - static, use `SensorMotionManager`. - - Detectable objects and sensor objects must have an attribute that is a GeodeticLocationState - OR CartesianLocationState - - Detectable objects, if they aren't Actors, could implement _get_detection_state() -> bool:` - to allow this class to ignore them sometimes. The default way is to use a `DetectabilityState` - on the actor. - - Sensor objects MUST implement these two methods: - 1. `entity_entered_range(object)` - 2. `entity_exited_range(object)` - - The first is called when an entity enters the sensor's visiblity. - The second is called when an entity leaves the visibility or becomes undetectable. - - The sensor object CAN implement a method called `detection_checker`. - That method takes the location of an object to detect and returns True/False. - - The motion manager will learn about sensor objects with: - - sensor_motion_manager.add_sensor(sensor_object, radius) - - Where radius is a distance in the units defined in upstage.STAGE. - - Simple usage: - >>> manager = SteppedMotionManager(timestep=0.1) - >>> UP.STAGE.motion_manager = manager - >>> ... - >>> manager.add_sensor(binoculars, 'vision_radius') - >>> manager.add_detectable(bird, 'location') - - # TODO: Unify sensor and movable - # TODO: Having only moving things be detectable/using `_start_mover` - is easy, but this class lets us do static detection easier, so we may - have to go about it differently. - # TODO: Data structures for efficient distances - """ - - def __init__(self, timestep: float, max_empty_events: int = 3, debug: bool = False) -> None: - """Create the Stepped motion manager. - - Args: - timestep (float): Timestep to do all pairs distance checks. - max_empty_events (int, optional): How many timesteps where no events causes a shutdown. - Defaults to 3. - debug (bool, optional): Record data or not. Defaults to False. - """ - super().__init__() - self._sensors: dict[SensorType, tuple[Callable[[], float], Callable[[], LOC_TYPES]]] = {} - self._detectables: dict[Actor, Callable[[], LOC_TYPES]] = {} - self._in_view: set[tuple[SensorType, Actor]] = set() - self._timestep = timestep - self._max_empty_events = max_empty_events - self._debug = debug - self._debug_log: list[Any] = [] - self._is_running = False - - def _do_log(self, msg: Any) -> None: - """Write to a log list. - - Args: - msg (Any): Anything to append. - """ - if self._debug: - self._debug_log.append(msg) - - def _update_awareness(self, sensor: SensorType, object: Actor, visible: bool) -> None: - """Modify sensor/object awareness. - - Args: - sensor (SensorType): Sensor - object (Actor): The sensed - visible (bool): If the sensed is visible. - """ - if visible: - if (sensor, object) not in self._in_view: - self._in_view.add((sensor, object)) - sensor.entity_entered_range(object) - else: - if (sensor, object) in self._in_view: - self._in_view.remove((sensor, object)) - sensor.entity_exited_range(object) - - def _test_detect(self, detectable: Actor) -> bool: - """Is an actor detectable? - - Args: - detectable (Actor): The detectable - - Returns: - bool: If it can be detected - """ - if not hasattr(detectable, "_get_detection_state"): - return True - detect_state = detectable._get_detection_state() - if detect_state is None: - return True - visibility: bool = getattr(detectable, detect_state) - return visibility - - @staticmethod - def _detect_dist(loc1: LOC_TYPES, radius: float, loc2: LOC_TYPES, sensor: SensorType) -> bool: - """Run a detectability check, including sensor custom function. - - Args: - loc1 (LOC_TYPES): Sensor location - radius (float): Sensor radius - loc2 (LOC_TYPES): Target location - sensor (SensorType): Sensor object - - Returns: - bool: If it's detectable - """ - if hasattr(sensor, "detection_checker"): - visible = sensor.detection_checker(loc2) - else: - dist = loc1.straight_line_distance(loc2) - visible = dist <= radius - return cast(bool, visible) - - def _run_detectable( - self, - sensor_req: list[SensorType] | None = None, - detectable_req: list[Actor] | None = None, - ) -> None: - """All pairs distance checking. - - Args: - sensor_req (list[SensorType] | None, optional): Sensors. Defaults to None. - detectable_req (list[Actor] | None, optional): Detectables. Defaults to None. - """ - sensor_req = list(self._sensors) if sensor_req is None else sensor_req - sensor_radii = [self._sensors[s][0]() for s in sensor_req] - sensor_locs = [self._sensors[s][1]() for s in sensor_req] - - detectable_req = list(self._detectables) if detectable_req is None else detectable_req - detectable_req = [d for d in detectable_req if self._test_detect(d)] - detect_locs = [self._detectables[d]() for d in detectable_req] - - for sensor, radius, loc in zip(sensor_req, sensor_radii, sensor_locs): - for detectable, d_loc in zip(detectable_req, detect_locs): - if detectable is sensor: - continue - visible = self._detect_dist(loc, radius, d_loc, sensor) - self._do_log((self.env.now, sensor, loc, detectable, d_loc)) - self._update_awareness(sensor, detectable, visible) - - def _only_event_test(self) -> bool: - """Determine if there are no events in the queue.""" - if len(self.env._queue) == 0: - return True - return False - - @process - def run(self) -> Generator[SimpyEvent, None, None]: - """Run the main stepped motion loop. - - Yields: - Generator[SimpyEvent, None, None]: _description_ - """ - if self._is_running: - # If run() is called later than a task is queued, - # then this may be true already. - return - self._is_running = True - n_empty = 0 - while True: - timeout = self.env.timeout(self._timestep) - yield timeout - self._run_detectable() - # prevents infinite simulations - if self._only_event_test(): - n_empty += 1 - if n_empty >= self._max_empty_events: - return - else: - n_empty = 0 - - @process - def run_particular(self, rate: float, detectable: Actor) -> Generator[SimpyEvent, None, None]: - """Run detections against a single target at a faster rate. - - Args: - rate (float): Time rate to do detection checks. - detectable (Actor): The actor to be detected by the known sensors. - """ - while True: - yield self.env.timeout(rate) - self._run_detectable(sensor_req=None, detectable_req=[detectable]) - - def add_sensor( - self, - sensor: SensorType, - radius_attr_name: str = "radius", - location_attr_name: str = "location", - ) -> None: - """Add a sensor the motion manager. - - Args: - sensor (SensorType): The sensing object - radius_attr_name (str): Radius attribute name. Defaults to "radius". - location_attr_name (str): Location attribute name. Defaults to "location". - """ - # test the sensor for earlier errors about improperly-defined methods - required_methods = ["entity_entered_range", "entity_exited_range"] - required_attrs = [radius_attr_name, location_attr_name] - for req in required_methods: - if not hasattr(sensor, req): - raise NotImplementedError(f"Sensor {sensor} does not have '{req}' method!") - for attr in required_attrs: - if not hasattr(sensor, attr): - raise SimulationError(f"Sensor {sensor} doesn't have attribute {attr}") - - def get_radius() -> float: - return cast(float, getattr(sensor, radius_attr_name)) - - def get_location() -> LOC_TYPES: - return cast(LOC_TYPES, getattr(sensor, location_attr_name)) - - self._sensors[sensor] = (get_radius, get_location) - - def add_detectable( - self, - detectable: Actor, - location_attr_name: str = "location", - new_rate: float | None = None, - ) -> None: - """Add an object that is detectable to the manager. - - The object must have an attribute that performs distance calculations. - See the class docstring for more. - - Args: - detectable (Actor): An object that has a location attribute - location_attr_name (str): Name of the location attribute. Defaults to "location". - new_rate (float | None): Optional new rate for a detectable - (if it needs faster, most likely) - """ - if not hasattr(detectable, location_attr_name): - raise SimulationError( - f"Detectable {detectable} doesn't have attribute {location_attr_name}" - ) - try: - self._test_detect(detectable) - except Exception: - raise SimulationError(f"Detectable {detectable} needs a detectable state.") - - def get_location() -> LOC_TYPES: - return cast(LOC_TYPES, getattr(detectable, location_attr_name)) - - self._detectables[detectable] = get_location - - def _mover_not_detectable(self, detectable: Actor) -> None: - """Called via DetectabilityState state when an object becomes undetectable. - - Could be called for any reason; use this feature to alert sensors that - an object should no longer be considered by that sensor. - """ - # TODO: key on detectable may make it faster - to_rem = set() - for sensor, detect in self._in_view: - if detect is detectable: - sensor.entity_exited_range(detectable) - to_rem.add((sensor, detect)) - self._in_view -= to_rem - - def _mover_became_detectable(self, detectable: Actor) -> None: - """Called via DetectabilityState state when an object becomes detectable. - - Could be called for any reason; use this feature to alert sensors that - an object should be considered by that sensor. - """ - self._run_detectable(detectable_req=[detectable]) - - def _start_mover(self, mover: Actor, speed: float, waypoints: list[LOC_TYPES]) -> None: - # we don't need this method, except to hook into motion states - if not self._is_running: - self.run() - if mover in self._detectables: - return - state_name_1 = mover._get_matching_state(GeodeticLocationChangingState) - state_name_2 = mover._get_matching_state(CartesianLocationChangingState) - - use_state = state_name_1 or state_name_2 - if use_state is None: - raise SimulationError(f"Mover {mover} doesn't have a Location state") - - self.add_detectable(mover, use_state) - - def _stop_mover(self, mover: Actor) -> None: - # we don't need this method, except to hook into motion states - # It may be useful for ending detections, but the user should - # handle that themselves - if mover in self._detectables: - del self._detectables[mover] diff --git a/src/upstage_des/nucleus.py b/src/upstage_des/nucleus.py deleted file mode 100644 index d04f4fa..0000000 --- a/src/upstage_des/nucleus.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""The file contains the Nucleus features of UPSTAGE.""" - -from collections import defaultdict -from typing import Any - -from upstage_des.actor import Actor -from upstage_des.base import UpstageError -from upstage_des.task_network import TaskNetwork - - -class NucleusInterrupt: - """A data container for interrupting nucleus events.""" - - def __init__(self, name: str, value: Any) -> None: - """A container for Nucleus interrupt data. - - Args: - name (str): Name of the state causing the interrupt. - value (Any): The state value - """ - self.state_name = name - self.value = value - - def __repr__(self) -> str: - return f"NucleusInterrupt: {self.state_name} {self.value}" - - -class TaskNetworkNucleus: - """The nucleus, for state-based task network signaling.""" - - def _attach(self) -> None: - """Attach the nucleus to an actor.""" - self._actor.log(f"Attaching {self} as a state listener!") - if self._actor._state_listener is not None: - raise UpstageError(f"{self._actor} already has a nucleus attached.") - self._actor._state_listener = self - - def __init__( - self, - *, - actor: Actor, - ) -> None: - """Create a task network nucleus on an Actor. - - Args: - actor (Actor): The actor instance. - """ - self._actor = actor - self._state_map: dict[str, set[str]] = defaultdict(set) - self._network_map: dict[str, set[str]] = defaultdict(set) - self._attach() - - def add_network( - self, - network_name: str | TaskNetwork, - watch_states: list[str], - ) -> None: - """Add a network to the nucleus for state management. - - Args: - network_name (str | TaskNetwork): A task network that works on this nucleus/actor - watch_states (list[str]): States that - when changed - cause the network to change. - """ - if isinstance(network_name, TaskNetwork): - network_name = network_name.name - if network_name not in self._actor._task_networks: - raise UpstageError(f"No network {network_name} in {self._actor}") - for state in watch_states: - self._state_map[state].add(network_name) - self._network_map[network_name].add(state) - # if not hasattr(actor, state): - # raise SimulationError(f"State {state} does not exist on actor") - - def remove_network( - self, - network_name: str | TaskNetwork, - ) -> None: - """Remove a network from nucleus. - - Args: - network_name (str | TaskNetwork): A task network that works on this nucleus/actor - """ - if isinstance(network_name, TaskNetwork): - network_name = network_name.name - if network_name not in self._actor._task_networks: - raise UpstageError(f"No network {network_name} in {self._actor}") - for state in self._network_map[network_name]: - self._state_map[state].remove(network_name) - del self._network_map[network_name] - - def send_change(self, state_name: str, state_value: Any) -> None: - """Send a change notification for a given state. - - Args: - state_name (str): The state's name. - state_value (Any): The value of the state. - """ - for net_name in self._state_map.get(state_name, []): - net: TaskNetwork | None = self._actor._task_networks.get(net_name, None) - if net is None: - raise UpstageError(f"No network {net_name} in {self._actor}") - proc = net._current_task_proc - if proc is not None: - proc.interrupt( - cause=NucleusInterrupt(state_name, state_value), - ) diff --git a/src/upstage_des/py.typed b/src/upstage_des/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/src/upstage_des/resources/__init__.py b/src/upstage_des/resources/__init__.py deleted file mode 100644 index bd3f061..0000000 --- a/src/upstage_des/resources/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""This sub-module contains advanced stores and containers for UPSTAGE.""" - -from .container import ( - ContainerEmptyError, - ContainerError, - ContainerFullError, - ContinuousContainer, -) -from .monitoring import ( - SelfMonitoringContainer, - SelfMonitoringContinuousContainer, - SelfMonitoringFilterStore, - SelfMonitoringReserveContainer, - SelfMonitoringSortedFilterStore, - SelfMonitoringStore, -) -from .reserve import ReserveContainer -from .sorted import SortedFilterGet, SortedFilterStore - -__all__ = [ - "ContinuousContainer", - "ContainerEmptyError", - "ContainerError", - "ContainerFullError", - "SelfMonitoringStore", - "SelfMonitoringFilterStore", - "SelfMonitoringContainer", - "SelfMonitoringContinuousContainer", - "SelfMonitoringSortedFilterStore", - "SelfMonitoringReserveContainer", - "ReserveContainer", - "SortedFilterStore", - "SortedFilterGet", -] diff --git a/src/upstage_des/resources/container.py b/src/upstage_des/resources/container.py deleted file mode 100644 index dc76d40..0000000 --- a/src/upstage_des/resources/container.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""This file contains a ContinuousContainer.""" - -from collections.abc import Callable, Generator -from typing import TYPE_CHECKING, Any - -from simpy import Environment, Event, Interrupt, Process -from simpy.core import BoundClass - -__all__ = ( - "ContinuousContainer", - "ContainerEmptyError", - "ContainerError", - "ContainerFullError", -) - -EMPTY_STATUS: str = "empty" -FULL_STATUS: str = "full" - - -class ContainerError(Exception): - """The container is in an invalid state.""" - - @property - def cause(self) -> Any: - """Get the exception's cause. - - Returns: - Any: The cause. - """ - return self.args[0] - - -class ContainerFullError(ContainerError): - """The container has reach or exceeded its capacity.""" - - pass - - -class ContainerEmptyError(ContainerError): - """The container is empty or has a negative level.""" - - pass - - -class _ContinuousEvent(Event): - def _run(self, runtime: float) -> Generator[Event, None, None]: - self.container.add_user(self) - do_remove = True - try: - evt = self.env.timeout(runtime) - yield evt - except Interrupt as interruption: - if interruption.cause == self.stop_cause: - # The container will handle this for us - do_remove = False - for callback in self.custom_callbacks: - callback() - elif interruption.cause != "stop": - raise Interrupt(interruption) - if do_remove: - self.container.remove_user(self) - - def __init__( - self, - container: "ContinuousContainer", - rate: float, - time: float, - stop_cause: str | None, - custom_callbacks: list[Callable[[], None]] | None = None, - ) -> None: - super().__init__(container.env) - self.container = container - self.rate = rate - self.custom_callbacks = custom_callbacks or [] - self.stop_cause = stop_cause - time = float("inf") if time is None else time - self.process = self.env.process(self._run(time)) - - def cancel(self) -> None: - """Cancel this request. - - This method has to be called if the put request must be aborted, for - example if a process needs to handle an exception like an - :class:`~simpy.events.Interrupt`. - - If the put request was created in a :keyword:`with` statement, this - method is called automatically. - - """ - if self.process.is_alive: - self.process.interrupt("stop") - - -class ContinuousPut(_ContinuousEvent): - """An event that puts *rate* per unit time into the *container*. - - Raise a :exc:`ValueError` if ``rate <= 0``. - """ - - def __init__( - self, - container: "ContinuousContainer", - rate: float, - time: float, - custom_callbacks: list[Callable[[], None]] | None = None, - ) -> None: - """Create a put event that is continuous. - - Args: - container (ContinuousContainer): Container to add to. - rate (float): Rate to add at. - time (float): Time to run the event. - custom_callbacks (list[Callable[[], None]] | None, optional): Callbacks - for completion. Defaults to None. - """ - if rate <= 0: - raise ValueError( - "Rates must be greater than zero. Put means 'positive'." - ) # pragma: no cover - super().__init__(container, rate, time, FULL_STATUS, custom_callbacks) - - -class ContinuousGet(_ContinuousEvent): - """An event that gets *amount* from the *container*.""" - - def __init__( - self, - container: "ContinuousContainer", - rate: float, - time: float, - custom_callbacks: list[Callable[[], None]] | None = None, - ) -> None: - """Create a get event that is continuous. - - Args: - container (ContinuousContainer): Container to take from. - rate (float): Rate to remote at. - time (float): Time to run the event. - custom_callbacks (list[Callable[[], None]] | None, optional): Callbacks - for completion. Defaults to None. - """ - if rate <= 0: - raise ValueError( - "Rates must be greater than zero. Get means 'negative'." - ) # pragma: no cover - super().__init__(container, -rate, time, EMPTY_STATUS, custom_callbacks) - - -class ContinuousContainer: - """A container that accepts continuous gets and puts.""" - - def __init__( - self, - env: Environment, - capacity: int | float, - init: int | float = 0.0, - error_empty: bool = True, - error_full: bool = True, - ): - """Create a container that allows continuous gets and puts. - - Args: - env (Environment): SimPy Environment. - capacity (int | float): Capacity of the container - init (int | float, optional): Initial amount. Defaults to 0.0. - error_empty (bool, optional): Error when it gets empty. Defaults to True. - error_full (bool, optional): Error when it gets full. Defaults to True. - """ - self._capacity = capacity - if init < 0 or capacity < 0: - raise ValueError("Initial and capacity cannot be negative.") # pragma: no cover - self._level = init - self.error_empty = error_empty - self.error_full = error_full - - self._rate: float = 0.0 - self._env: Environment = env - self._last: float = self._env.now - self._active_users: list[_ContinuousEvent] = [] - self._checking: Process | None = None - - if TYPE_CHECKING: - - def put( - self, - rate: float, - time: float, - custom_callbacks: list[Callable[[], None]] | None = None, - ) -> ContinuousPut: - """Request to put *item* into the store.""" - return ContinuousPut(self, rate, time, custom_callbacks) - - def get( - self, rate: float, time: float, custom_callbacks: list[Callable[[], None]] | None = None - ) -> ContinuousGet: - """Request to get an *item* out of the store.""" - return ContinuousGet(self, rate, time, custom_callbacks) - - else: - put = BoundClass(ContinuousPut) - get = BoundClass(ContinuousGet) - - def time_until_level(self, level: float, rate: float = 0.0) -> float: - """Calculate the time until the containers reaches a value. - - Args: - level (float): The value to reach. - rate (float, optional): Additional rate. Defaults to 0.0. - - Returns: - float: The time to reach the level. - """ - rate += self._rate - - if self.level == level: - return 0.0 # pragma: no cover - elif rate == 0: - return float("inf") # pragma: no cover - time = (level - self._level) / rate - return time if time > 0 else float("inf") - - def time_until_done(self, rate: float = 0.0) -> float: - """Calculate the time until the container is full or empty. - - Args: - rate (float, optional): Additional rate. Defaults to 0.0. - - Returns: - float: Time until the container reaches a limit. - """ - rate += self._rate - if rate > 0: - return (self.capacity - self.level) / rate - elif rate < 0: - return -self.level / rate - else: - return float("inf") # pragma: no cover - - @property - def env(self) -> Environment: - """Get the environment of the container. - - Returns: - Environment: The SimPy environment. - """ - return self._env - - @property - def rate(self) -> float: - """Get the current net rate. - - Returns: - float: The net rate. - """ - return self._rate - - @property - def capacity(self) -> float: - """Get the capacity of the container. - - Returns: - float: The capacity. - """ - return self._capacity - - @property - def _active_puts(self) -> list[ContinuousPut]: - puts = [] - for x in self._active_users: - if x.rate > 0: - assert isinstance(x, ContinuousPut) - puts.append(x) - return puts - - @property - def _active_gets(self) -> list[ContinuousGet]: - gets = [] - for x in self._active_users: - if x.rate < 0: - assert isinstance(x, ContinuousGet) - gets.append(x) - return gets - - def _set_level(self) -> float: - """Set the level of the container based on the active gets/puts. - - Returns: - float: The current level. - """ - now = self._env.now - if now > self._last: - self._level += self._rate * (now - self._last) - self._last = now - return self._level - - def _check_empty(self) -> tuple[list[ContinuousGet], float]: - level = self._level - to_rem: list[ContinuousGet] = [] - rate = 0.0 - if level > 0: - return to_rem, rate - for get in tuple(self._active_gets): - get.process.interrupt(EMPTY_STATUS) - to_rem.append(get) - rate += -get.rate - if level == 0 and self.error_empty: - raise ContainerEmptyError("Container is empty!") - elif level < 0.0: - raise ContainerError(f"Container level is less than 0 ({level:.3f})!") - return to_rem, rate - - def _check_full(self) -> tuple[list[ContinuousPut], float]: - level = self._level - to_rem: list[ContinuousPut] = [] - rate: float = 0.0 - if level < self.capacity: - return to_rem, rate - for put in tuple(self._active_puts): - put.process.interrupt(FULL_STATUS) - to_rem.append(put) - rate += -put.rate - if level == self.capacity and self.error_full: - raise ContainerFullError("Container is full!") - elif level > self.capacity: - msg = "Container level exceeds capacity by {:.3f}!" - raise ContainerError(msg.format(level - self._capacity)) - return to_rem, rate - - def _check(self) -> Generator[Event, None, None]: - if not self._rate or self._rate == 0: - return # pragma: no coverd - - level = self.level - - check_wait = ((self._capacity if self._rate > 0 else 0.0) - level) / self._rate - - try: - yield self._env.timeout(check_wait) - except Interrupt as interruption: - if interruption.cause != "updated": - raise # pragma: no cover - finally: - to_rem: list[ContinuousGet] | list[ContinuousPut] = [] - rate: float - self._set_level() - if self._rate < 0: - to_rem, rate = self._check_empty() - elif self._rate > 0: - to_rem, rate = self._check_full() - if to_rem: - for req in to_rem: - self._active_users.remove(req) - self._add_rate(rate, interrupt=False) - - def _add_rate(self, rate_change: float | int, interrupt: bool = True) -> None: - """Add a new rate to the existing rate.""" - if self._checking is not None and interrupt: - self._checking.interrupt("updated") - - self._set_level() - self._rate += rate_change - - if self._rate == 0.0: - self._checking = None - else: - self._checking = self._env.process(self._check()) - - def add_user(self, user: _ContinuousEvent) -> None: - """Add a user to the container. - - Args: - user (_ContinuousEvent): The user event - """ - self._active_users.append(user) - self._add_rate(user.rate) - - def remove_user(self, user: _ContinuousEvent) -> None: - """Remove a user of the container. - - Args: - user (_ContinuousEvent): The user event. - """ - to_remove_rate = -1 * user.rate - self._active_users.remove(user) - self._add_rate(to_remove_rate) - - def _set_new_rate(self, rate: float | int) -> None: - """Set a new rate. - - Args: - rate (float | int): The new total rate. - """ - curr_rate = self.rate - diff = rate - curr_rate - self._add_rate(diff) - - @property - def level(self) -> float: - """Get the level of the container. - - Returns: - float: The current amount remaining. - """ - return self._level + self._rate * (self._env.now - self._last) diff --git a/src/upstage_des/resources/monitoring.py b/src/upstage_des/resources/monitoring.py deleted file mode 100644 index 8047930..0000000 --- a/src/upstage_des/resources/monitoring.py +++ /dev/null @@ -1,337 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Stores that monitor/record their items over time.""" - -from collections.abc import Callable -from typing import Any - -from simpy import Container, Environment, Event, FilterStore, Store -from simpy.resources.container import ContainerGet, ContainerPut -from simpy.resources.store import FilterStoreGet, StoreGet, StorePut - -from upstage_des.base import SPECIAL_ENTITY_CONTEXT_VAR, NamedUpstageEntity - -from .container import ContinuousContainer -from .reserve import ReserveContainer -from .sorted import SortedFilterStore, _SortedFilterStoreGet - -__all__ = ( - "SelfMonitoringStore", - "SelfMonitoringFilterStore", - "SelfMonitoringContainer", - "SelfMonitoringContinuousContainer", - "SelfMonitoringSortedFilterStore", - "SelfMonitoringReserveContainer", - "MonitoringMixin", -) - -RECORDER_FUNC = Callable[[list[Any]], int] -MONITORING_ENTITY_GROUP = "monitored" - - -class MonitoringMixin(NamedUpstageEntity, skip_classname=True): - """Base class for matching Monitored types.""" - - name: str | None - _quantities: list[tuple[float, Any]] - - def _add_special_group(self) -> None: - """Add self the the monitored context group. - - Called by the NamedUpstageEntity on group inits. - """ - ans = SPECIAL_ENTITY_CONTEXT_VAR.get().monitored - if self in ans: - return - ans.append(self) - - -class SelfMonitoringStore( - MonitoringMixin, - Store, - skip_classname=True, -): - """A self-monitoring version of the SimPy Store.""" - - def __init__( - self, - env: Environment, - capacity: float | int = float("inf"), - item_func: RECORDER_FUNC | None = None, - name: str | None = None, - ) -> None: - """A monitoring version of the SimPy Store. - - Since Stores contain a list of objects, recording the mutable list is replaced - by recording the length of the list by default. Otherwise, input the item_func - to take the store items and return a recordable value. - - Args: - env (Environment): SimpyEnvironment. - capacity (float | int, optional): Capacity of the store. Defaults to float("inf"). - item_func (RECORDER_FUNC | None, optional): Function to create recorded values. - Defaults to None. - name (str, optional): The name of the store, if it doesn't exist as a state. - Defaults to None. - """ - super().__init__(env, capacity=capacity) - self.name = name - self.item_func = item_func if item_func is not None else len - self._quantities = [(self._env.now, self.item_func(self.items))] - - def _record(self, call: str) -> None: - v = self.item_func(self.items) - if v != self._quantities[-1][1] or call == "environment": - self._quantities.append((self._env.now, v)) - - def _trigger_put(self, event: Event) -> None: # type: ignore [override] - super()._trigger_put(event) - self._record("put") - - def _trigger_get(self, event: Event) -> None: # type: ignore [override] - super()._trigger_get(event) - self._record("get") - - def _do_put(self, event: StorePut) -> None: - super()._do_put(event) - if event.triggered: - self._record("put") - return None - - def _do_get(self, event: StoreGet) -> None: - super()._do_get(event) - if event.triggered: - self._record("get") - return None - - -class SelfMonitoringFilterStore( - MonitoringMixin, - FilterStore, - skip_classname=True, -): - """A self-monitoring version of the SimPy FilterStore.""" - - def __init__( - self, - env: Environment, - capacity: float | int = float("inf"), - item_func: RECORDER_FUNC | None = None, - name: str | None = None, - ) -> None: - """A monitoring version of the SimPy FilterStore. - - Since Stores contain a list of objects, recording the mutable list is replaced - by recording the length of the list by default. Otherwise, input the item_func - to take the store items and return a recordable value. - - Args: - env (Environment): SimpyEnvironment. - capacity (float | int, optional): Capacity of the store. Defaults to float("inf"). - item_func (RECORDER_FUNC | None, optional): Function to create recorded values. - Defaults to None. - name (str, optional): The name of the store, if it doesn't exist as a state. - Defaults to None. - """ - super().__init__(env, capacity=capacity) - self.name = name - self.item_func = item_func if item_func is not None else len - self._quantities = [(self._env.now, self.item_func(self.items))] - - def _record(self, call: str) -> None: - v = self.item_func(self.items) - if v != self._quantities[-1][1] or call == "environment": - self._quantities.append((self._env.now, v)) - - def _trigger_put(self, event: Event) -> None: # type: ignore [override] - super()._trigger_put(event) - self._record("put") - - def _trigger_get(self, event: Event) -> None: # type: ignore [override] - super()._trigger_get(event) - self._record("get") - - def _do_put(self, event: StorePut) -> None: - super()._do_put(event) - if event.triggered: - self._record("put") - - def _do_get(self, event: FilterStoreGet) -> bool | None: # type: ignore [override] - ans = super()._do_get(event) - if event.triggered: - self._record("get") - return ans - - -class SelfMonitoringContainer( - MonitoringMixin, - Container, - skip_classname=True, -): - """A self-monitoring version of the SimPy Container.""" - - def __init__( - self, - env: Environment, - capacity: float = float("inf"), - init: float = 0.0, - name: str | None = None, - ) -> None: - """A monitoring version of a SimPy container. - - Args: - env (Environment): SimPy environment. - capacity (float, optional): Capacity of the container. Defaults to float("inf"). - init (float, optional): Initial amount. Defaults to 0.0. - name (str, optional): The name of the store, if it doesn't exist as a state. - Defaults to None. - """ - super().__init__(env, capacity=capacity, init=init) - self.name = name - self._quantities: list[tuple[float, float]] = [(self._env.now, self._level)] - - def _record(self) -> None: - reading = (self._env.now, self._level) - if reading != self._quantities[-1]: - self._quantities.append(reading) - - def _trigger_put(self, event: Event) -> None: # type: ignore [override] - super()._trigger_put(event) - self._record() - - def _trigger_get(self, event: Event) -> None: # type: ignore [override] - super()._trigger_get(event) - self._record() - - def _do_put(self, event: ContainerPut) -> bool | None: - ans = super()._do_put(event) - if event.triggered: - self._record() - return ans - - def _do_get(self, event: ContainerGet) -> bool | None: - ans = super()._do_get(event) - if event.triggered: - self._record() - return ans - - -class SelfMonitoringContinuousContainer( - MonitoringMixin, - ContinuousContainer, - skip_classname=True, -): - """A self-monitoring version of the Continuous Container.""" - - def __init__( - self, - env: Environment, - capacity: int | float, - init: int | float = 0.0, - error_empty: bool = True, - error_full: bool = True, - name: str | None = None, - ) -> None: - """A monitoring version of the Continuous container. - - The container allows continuous gets and puts. - - Args: - env (Environment): SimPy Environment. - capacity (int | float): Capacity of the container - init (int | float, optional): Initial amount. Defaults to 0.0. - error_empty (bool, optional): Error when it gets empty. Defaults to True. - error_full (bool, optional): Error when it gets full. Defaults to True. - name (str, optional): The name of the store, if it doesn't exist as a state. - Defaults to None. - """ - super().__init__(env, capacity, init, error_empty, error_full) - self.name = name - self._quantities = [(self._env.now, self._level)] - - def _set_level(self) -> float: - """Set the level of the container based on the active gets/puts. - - Returns: - float: The current level. - """ - amt = super()._set_level() - now = self._env.now - if (now, amt) != self._quantities[-1]: - self._quantities.append((now, amt)) - return amt - - -class SelfMonitoringSortedFilterStore(SortedFilterStore, SelfMonitoringStore, skip_classname=True): - """A self-monitoring version of the SortedFilterStore.""" - - def _do_get(self, event: _SortedFilterStoreGet) -> bool: # type: ignore [override] - ans = super()._do_get(event) - if event.triggered: - self._record("get") - return ans - - -class SelfMonitoringReserveContainer( - MonitoringMixin, - ReserveContainer, - skip_classname=True, -): - """A self-monitoring version of the ReserveContainer.""" - - def __init__( - self, - env: Environment, - capacity: float = float("inf"), - init: float = 0.0, - name: str | None = None, - ) -> None: - """Create a store-like object that allows reservations, and records. - - Note that this store doesn't actually yield to SimPy when requesting. - - Use it to determine if anything is avaiable for reservation, but there is no - queue for getting a reservation. - - Args: - env (Environment): The SimPy Environment - init (float, optional): Initial amount available. Defaults to 0.0. - capacity (float, optional): Total capacity. Defaults to float("inf"). - name (str, optional): The name of the store, if it doesn't exist as a state. - Defaults to None. - """ - super().__init__(env, init, capacity) - self.name = name - self._quantities = [(env.now, init)] - - def _record(self) -> None: - """Record the level of the store.""" - now = self._env.now - data = (now, self._real_level) - if data != self._quantities[-1]: - self._quantities.append(data) - - def take(self, requester: Any) -> float: - """Take some amount from the store, by a requester. - - Args: - requester (Any): The entity requesting the amount - - Returns: - float: The aount requested. - """ - amt = super().take(requester) - self._record() - return amt - - def put(self, amount: float, capacity_increase: bool = False) -> None: - """Put some amount into the store. - - Args: - amount (float): The amount to put - capacity_increase (bool, optional): Allow capacity to increase. Defaults to False. - """ - super().put(amount, capacity_increase) - self._record() diff --git a/src/upstage_des/resources/reserve.py b/src/upstage_des/resources/reserve.py deleted file mode 100644 index 80416ea..0000000 --- a/src/upstage_des/resources/reserve.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""This file contains a Container that allows reservations.""" - -from collections.abc import Generator -from typing import Any - -from simpy import Environment, Event - -from ..task import process - -__all__ = ("ReserveContainer",) - - -class ReserveContainer: - """A store that allows requests to be scheduled in advance. - - This is not a true container (you can't yield on a reserve slot!). - """ - - def __init__( - self, - env: Environment, - init: float = 0.0, - capacity: float = float("inf"), - ) -> None: - """Create a container-like object that allows reservations. - - Note that this container doesn't actually yield to SimPy when requesting. - - Use it to determine if anything is avaiable for reservation, but there is no - queue for getting a reservation. - - Args: - env (Environment): The SimPy Environment - init (float, optional): Initial amount available. Defaults to 0.0. - capacity (float, optional): Total capacity. Defaults to float("inf"). - """ - self.capacity = capacity - self._env = env - self._level = init - self._real_level = init - self._queued: dict[Any, tuple[float, float]] = {} - - @property - def remaining(self) -> float: - """Return the amount remaining in the container. - - Returns: - float: Amount remaining - """ - return self._level - - @property - def available(self) -> float: - """Return the amount remaining in the container. - - Returns: - float: Amount remaining. - """ - return self.remaining - - @property - def queued(self) -> list[Any]: - """Get the queued requesters. - - Returns: - list[Any]: List of requesters. - """ - return list(self._queued.keys()) - - @process - def _expire_request(self, requester: Any, time: float) -> Generator[Event, None, None]: - """Expire the request after an expiration period or at a specific time. - - :param request: the Request namedtuple object - :param expiration: the expiration Event object - - :type request: - :type expiration: - - """ - yield self._env.timeout(time) - self.cancel_request(requester) - - def reserve(self, requester: Any, quantity: float, expiration: float | None = None) -> bool: - """Reserve a quantity of storage.""" - if self.available < quantity: - return False - elif requester not in self._queued: - self._level -= quantity - self._queued[requester] = (quantity, self._env.now) - if expiration is not None: - self._expire_request(requester, expiration) - return True - else: - return False - - def cancel_request(self, requester: Any) -> bool: - """Have a request cancelled.""" - if requester not in self._queued: - return False - else: - request = [x for x in self._queued if x is requester] - if not request: - raise ValueError("Requester not available to cancel") - self._level += self._queued[requester][0] - self._queued.pop(request[0]) - return True - - def take(self, requester: Any) -> float: - """If in queue, allow requester take the actual quantity.""" - if requester not in self._queued: - raise ValueError("Requester is not in queue, cannot take.") - else: - amt, _ = self._queued.pop(requester) - self._real_level -= amt - return amt - - def put(self, amount: float, capacity_increase: bool = False) -> None: - """Put some quantity back in.""" - new = self._level + amount - if new > self.capacity and not capacity_increase: - raise ValueError("Adding too much.") - else: - self._level = new - self._real_level += amount diff --git a/src/upstage_des/resources/sorted.py b/src/upstage_des/resources/sorted.py deleted file mode 100644 index 576ce0b..0000000 --- a/src/upstage_des/resources/sorted.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Filter stores that allow sorting of items.""" - -from collections.abc import Callable -from typing import TYPE_CHECKING, Any - -from simpy import Store -from simpy.resources.base import BoundClass, Get - -from ..events import Get as UPGet - -__all__ = ("SortedFilterStore", "SortedFilterGet") - - -class _SortedFilterStoreGet(Get): - """A getter for a custom store to retrieve items using filters and priorities. - - Request to get an *item* from the *store* matching the *filter* and - minimizing the *sorter*. The request is triggered once there is such an - item available in the store. - - *filter* should return ``True`` for items matching the filter criterion. - The default function returns ``True`` for all items, which makes the - request behave exactly like :class:`StoreGet`. - - *sorter* is a function receiving one item. It should return a value that - is to be minimized among the filter items. The default function is to not - sort, which makes the request behave exactly like :class:`FilterStoreGet`. - - :param resource: - :param filter: filter function for one item - :param sorter: sort function for one item - - :type resource: - :type filter: function - :type sorter: function - - """ - - def __init__( - self, - resource: "SortedFilterStore", - filter: Callable[[Any], bool] = lambda item: True, - sorter: Callable[[Any], tuple[Any, ...]] | None = None, - reverse: bool = False, - ): - self.filter = filter - self.sorter = sorter - self.reverse = reverse - super().__init__(resource) - - -class SortedFilterStore(Store): - """A store that supports the filtered and sorted retrieval of items. - - Resource with *capacity* slots for storing arbitrary objects supporting - filtered and sorted get requests. Like the :class:`Store`, the *capacity* - is unlimited by default and objects are put and retrieved from the store in - a first-in first-out order. - - Get requests can be customized with a filter function to only trigger for - items for which said filter function returns ``True``. They can further be - customized with a sorter function that prioritizes which of the filtered - items are to be returned. The prioritization happens through a - minimization. - - """ - - # Request to get an *item* for which *filter* returns ``True`` and for - # which *sorter* returns the minimum value out of the store. - if TYPE_CHECKING: - - def get(self) -> _SortedFilterStoreGet: # type: ignore[override] - """Request to get an *item* out of the store.""" - return _SortedFilterStoreGet(self) - - else: - get = BoundClass(_SortedFilterStoreGet) - - def _do_get(self, event: _SortedFilterStoreGet) -> bool: # type: ignore[override] - min_item: Any = None - # min_val is a tuple, in case the sorter returns an iterable - min_val: tuple[Any, ...] | None = None - for item in self.items: - if event.filter(item): - if event.sorter is not None: - val = event.sorter(item) - if min_val is None or (val > min_val if event.reverse else val < min_val): - min_item = item - min_val = val - else: - min_item = item - break - if min_item: - self.items.remove(min_item) - event.succeed(min_item) - return True - - -class SortedFilterGet(UPGet): - """A Get for a SortedFilterStore.""" - - def __init__( - self, - get_location: SortedFilterStore, - filter: Callable[[Any], bool] = lambda item: True, - sorter: Callable[[Any], tuple[Any, ...]] | None = None, - reverse: bool = False, - rehearsal_time_to_complete: float = 0.0, - ) -> None: - """Create a Get request on a SortedFilterStore. - - The filter function returns a boolean (True/False for in/out of consideration). - - The sorter function must return something sortable (number, tuple, e.g.) - - Args: - get_location (SIM.Store | SIM.Container): The place for the Get request - filter (Callable[[Any], bool]): The function that filters items in the store. - sorter (Callable[[Any], Any]): The function that returns values to sort an item on. - reverse (bool, optional): Whether to reverse the sort to be ascending. - rehearsal_time_to_complete (float, optional): _description_. Defaults to 0.0. - """ - super().__init__( - get_location=get_location, - rehearsal_time_to_complete=rehearsal_time_to_complete, - filter=filter, - sorter=sorter, - reverse=reverse, - ) diff --git a/src/upstage_des/routines.py b/src/upstage_des/routines.py deleted file mode 100644 index 1a6ac68..0000000 --- a/src/upstage_des/routines.py +++ /dev/null @@ -1,178 +0,0 @@ -"""A routine is something small done by an Actor in a Task.""" - -from collections.abc import Generator -from typing import Any - -import simpy as SIM - -from upstage_des.base import SIMPY_GEN, SettableEnv, SimulationError -from upstage_des.events import Any as AnyEvent -from upstage_des.events import BaseEvent, Get, Put, Wait - -ROUTINE_GEN = Generator[BaseEvent, Any, Any] - - -class Routine(SettableEnv): - """A base class for creating simple routines from. - - Routines are meant to be subclassed, and if you want actor or other data, - do that at instantiation. - - .. code-block:: python - - class SomeTask(Task): - def task(self, *, actor): - result = yield Routine(...) - do_something_with(result) - """ - - def __init__(self) -> None: - """Create the routine.""" - super().__init__() - - def run(self) -> ROUTINE_GEN: - """Define the routine.""" - raise NotImplementedError( - "User must define the actions performed when executing this routine" - ) - - def cancel(self) -> ROUTINE_GEN: - """Define how to clean up if the routine is interrupted.""" - raise NotImplementedError( - "User must define the actions performed when executing this routine" - ) - - def _run(self) -> ROUTINE_GEN: - try: - gen = self.run() - while True: - evt = next(gen) - if not isinstance(evt, BaseEvent): - raise SimulationError("Routines only support BaseEvent subclasses.") - yield evt - except StopIteration as e: - return e.value - except GeneratorExit: - # The parent task will close this generator and handle everything - # else. - ... - - def _run_cancel(self) -> SIMPY_GEN: - # Cancel can yield upstage events, so this pushes those through. - # This can be very unsafe - if a store/container can't handle a put - # or a get of some kind, it will hold forever. - for evt in self.cancel(): - yield evt.as_event() - - def rehearse(self) -> tuple[float, Any | None]: - """Rehearse the Routine. - - By default, this just rehearses the events in run() and returns the time - and no returned value. Routines do not expect to have a return value - anyway, but they may attach a value to themselves. - - Subclasses can call this and add a value, or do their own. - - Returns: - TASK_GEN: _description_ - - Yields: - tuple[float, Any | None]: Time and value of rehearsal. - """ - time = 0.0 - for evt in self._run(): - t, _ = evt.rehearse() - time += t - return time, None - - -class WindowedGet(Routine): - """A routine for repeating a Get request in a time window. - - If you're a waiting room, you might want to wait 5 minutes until - the first patient arrives to see if there are any others before you act, for - example. This routine will help do that in a compact way. - - It assumes that, on an interrupt or cancellation, you would put everything - back in the store. - """ - - def __init__( - self, - store: SIM.Store, - timeout: float, - timeout_unit: str | None = None, - reset_window: bool = False, - get_args: tuple[Any, ...] | None = None, - get_kwargs: dict[str, Any] | None = None, - ) -> None: - """Create a windowed Get request. - - The request will repeat within a time window until the time is done. - - If you opt to reset the window, instead of getting all possible gets - within 5 minutes, every new get resets the clock to 5 minutes. This may - cause infinite waiting in an edge case. - - Args: - store (SIM.Store): The store to get from. - get_args (tuple[Any, ...]): Arguments for the get request - get_kwargs (dict[str, Any]): Keyword arguments for the get request. - timeout (float): Time to wait from a successful get to end - timeout_unit (str | None, optional): Units of the timeout. - Defaults to None. - reset_window (bool, optional): If we restart the window on later successes. - Defaults to False. - """ - super().__init__() - self.store = store - self.timeout = timeout - self.timeout_unit = timeout_unit - self.reset_window = reset_window - self.get_args = [] if get_args is None else get_args - self.get_kwargs = {} if get_kwargs is None else get_kwargs - self.result: list[Any] = [] - self._evt: None | Get = None - - def run(self) -> ROUTINE_GEN: - """Run the windowed get.""" - need_wait = False - wait: Wait | None = None - while True: - incoming = Get(self.store, *self.get_args, **self.get_kwargs) - self._evt = incoming - - evts: list[BaseEvent] = [incoming] - if need_wait: - if wait is None or self.reset_window: - wait = Wait(self.timeout, timeout_unit=self.timeout_unit) - evts.append(wait) - - yield AnyEvent(*evts) - - if not incoming.is_complete(): - incoming.cancel() - break - - need_wait = True - obj = incoming.get_value() - self.result.append(obj) - return self.result - - def cancel(self) -> ROUTINE_GEN: - """Return all the items to the store and cancel the get.""" - while self.result: - yield Put(self.store, self.result.pop(0)) - - def rehearse(self) -> tuple[float, Any]: - """Rehearse the windowed get. - - This just expects one return value at the expected time of the get. - - Returns: - tuple[float, Any | None]: _description_ - """ - get = Get(self.store, *self.get_args, **self.get_kwargs) - t, v = get.rehearse() - self.result = [v] - return t, self.result diff --git a/src/upstage_des/state_proxies.py b/src/upstage_des/state_proxies.py deleted file mode 100644 index fb6304c..0000000 --- a/src/upstage_des/state_proxies.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Helpers and proxies for States.""" - -from collections.abc import ItemsView, Iterable, KeysView, ValuesView -from dataclasses import is_dataclass -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast - -VTD = TypeVar("VTD") -TDC = TypeVar("TDC") - - -if TYPE_CHECKING: - from .actor import Actor # pragma: no cover - from .states import _KeyValueBase # pragma: no cover - - -class _DictionaryProxy(Generic[VTD]): - """An in-between way to have dictionary-like access on descriptors.""" - - def __init__( - self, - descriptor: "_KeyValueBase", - instance: "Actor", - wrapped: dict[str, VTD], - ): - self.descriptor = descriptor - self.instance = instance - self._wrapped = wrapped - - def __setitem__(self, key: str, value: VTD) -> None: - self._wrapped[key] = value - if not self.descriptor._type_check(value, throw=False): - raise TypeError(f"Bad type for dictionary: {key}: {value}") - self.descriptor._record_state(self.instance, key, all=False) - - def setdefault(self, key: str, value: VTD) -> VTD: - if key in self._wrapped: - return self._wrapped[key] - self.__setitem__(key, value) - return value - - def items(self) -> ItemsView[str, VTD]: - """Get the items from the proxied dictionary. - - Returns: - list[tuple[str, MST]] - """ - return self._wrapped.items() - - def keys(self) -> KeysView[str]: - """Get the keys from the proxied dictionary. - - Returns: - list[str] - """ - return self._wrapped.keys() - - def values(self) -> ValuesView[VTD]: - """Get the items from the proxied dictionary. - - Returns: - list[MST] - """ - return self._wrapped.values() - - def raw_object(self) -> dict[str, VTD]: - """Give access to the underlying object. - - Any operations on that object won't be recorded. - - Returns: - dict[str, VTD]: The dictionary - """ - return self._wrapped - - def __getitem__(self, key: str) -> VTD: - return self._wrapped[key] - - def __iter__(self) -> Iterable[str]: - yield from self._wrapped - - def __contains__(self, key: str) -> bool: - return key in self._wrapped - - def __len__(self) -> int: - return len(self._wrapped) - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, dict): - return False - return other == self._wrapped - - def __repr__(self) -> str: - return repr(self._wrapped) - - -class _DataclassProxy(Generic[TDC]): - def __init__(self, descr: "_KeyValueBase", instance: "Actor", wrapped: TDC): - self._descr = descr - self._inst = instance - if not is_dataclass(wrapped): - raise TypeError("Wrapped value is not dataclass") - self._wrapped = wrapped - - @property - def __dataclass_fields__(self) -> Any: - """Support asking for fields() on the proxy.""" - return self._wrapped.__dataclass_fields__ - - def _typecheck(self, name: str, value: Any) -> None: - field_type = self._wrapped.__annotations__[name] - if not isinstance(value, field_type): - raise TypeError( - f"Dataclass value {value} for field {name} doesn't match type: {field_type}." - ) - - def raw_object(self) -> TDC: - """Give access to the underlying object. - - Any operations on that object won't be recorded. - - Returns: - TDC: The dataclass object - """ - return cast(TDC, self._wrapped) - - def __getattr__(self, name: str) -> Any: - return getattr(self._wrapped, name) - - def __setattr__(self, name: str, value: Any) -> None: - if name in ["_wrapped", "_descr", "_inst"]: - super().__setattr__(name, value) - elif is_dataclass(self._wrapped): - if hasattr(self._wrapped, name): - self._typecheck(name, value) - setattr(self._wrapped, name, value) - self._descr._record_state(self._inst, name, all=False) - - def __eq__(self, other: Any) -> bool: - if not is_dataclass(other): - return False - return other == self._wrapped - - def __repr__(self) -> str: - return repr(self._wrapped) diff --git a/src/upstage_des/state_sharing.py b/src/upstage_des/state_sharing.py deleted file mode 100644 index 5867c2f..0000000 --- a/src/upstage_des/state_sharing.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""States that enable sharing between tasks.""" - -from upstage_des.actor import Actor -from upstage_des.base import UpstageError -from upstage_des.states import ActiveState -from upstage_des.task import Task - - -class SharedLinearChangingState(ActiveState[float]): - """A state whose value changes linearly over time. - - Allows for multiple users of the state, keyed on actor and task. - - Assumes it's a non-frozen, floating-point value. - - Still activated in the usual way: - - >>> class Example(Actor): - >>> fuel = SharedLinearChangingState() - ... - >>> example.activate_state( - >>> state="fuel", - >>> task=self, - >>> rate=actor.fuel_burn, - >>> ) - """ - - def __init__( - self, - *, - default: float | None = None, - recording: bool = False, - ) -> None: - """Create a linear changing state that is shareable. - - Args: - default (float | None, optional): Default value. Defaults to None. - recording (bool, optional): If the state records. Defaults to False. - """ - super().__init__( - default=default, - frozen=False, - valid_types=float, - recording=recording, - default_factory=None, - ) - self.IGNORE_LOCK: bool = True - - def _active(self, instance: Actor) -> float | None: - """Return a value to set based on time or some other criteria. - - Args: - instance (Actor): The actor instance of the state - - Returns: - float: The value of the state - """ - # Note the task needs to default to none, so when we call 'active' - # to update the rate, we aren't adding a new one - data = self.get_activity_data(instance) - now: float = data["now"] - current: float = data["value"] - rate_tasks: dict[Task, float] = data.get("_rate_tasks", {}) - started_at: float | None = data.get("started_at", None) - if started_at is None: - # it's not currently active - return None - # no matter what, update the value - curr_rate = sum(rate_tasks.values()) - - last_calc_time: float = data.get("_last_time", now) - elapsed = now - last_calc_time - change = elapsed * curr_rate - new_value = current + change - - # Task shows up from the activation - if "task" in data: - rate_to_add: float = data["rate"] - task: Task = data["task"] - if task in rate_tasks: - raise UpstageError( - f"Duplicate task setting a rate {task}" - f"setting {self.name} on {instance}." - "You may have forgotten to deactivate." - ) - rate_tasks[task] = rate_to_add - - self.__set__(instance, new_value) - instance._set_active_state_data( - state_name=self.name, - started_at=now, - _rate_tasks=rate_tasks, - _last_time=now, - ) - return new_value - - def deactivate(self, instance: Actor, task: Task | None = None) -> bool: - """Deactivate the state. - - Args: - instance (Actor): Actor the state is on - task (Task): The task that is stopping its rate. - - Returns: - bool: Still active or not - """ - if task is None: - raise UpstageError("Unexpected rate deactivation without a task.") - data = self.get_activity_data(instance) - rate_tasks: dict[Task, float] = data.get("_rate_tasks", {}) - # Since this state is ignoring the lock, - # we can get here without it being active. - if task not in rate_tasks: - raise UpstageError(f"Task {task} is not changing {self.name} on {instance}") - del rate_tasks[task] - instance._set_active_state_data( - state_name=self.name, - _rate_tasks=rate_tasks, - ) - # force a re-calculation - getattr(instance, self.name) - if rate_tasks: - return True - return False diff --git a/src/upstage_des/states.py b/src/upstage_des/states.py deleted file mode 100644 index 1d70d72..0000000 --- a/src/upstage_des/states.py +++ /dev/null @@ -1,1527 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""A state defines the conditions of an actor over time.""" - -from abc import abstractmethod -from collections.abc import Callable, Iterable -from copy import deepcopy -from dataclasses import fields, replace -from enum import Enum -from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar, cast, runtime_checkable - -from simpy import Container, Environment, Store - -from upstage_des.base import SimulationError, UpstageError -from upstage_des.data_types import CartesianLocation, GeodeticLocation -from upstage_des.math_utils import _vector_add, _vector_subtract -from upstage_des.resources.monitoring import SelfMonitoringStore -from upstage_des.state_proxies import _DataclassProxy, _DictionaryProxy -from upstage_des.task import Task - -if TYPE_CHECKING: - from upstage_des.actor import Actor - - -__all__ = ( - "ActiveState", - "State", - "LinearChangingState", - "CartesianLocationChangingState", - "GeodeticLocationChangingState", - "ResourceState", - "DetectabilityState", - "CommunicationStore", -) - -CALLBACK_FUNC = Callable[["Actor", Any], None] - -ST = TypeVar("ST") - -RECORD_FUNC = Callable[[float, ST], Any] - - -@runtime_checkable -class RecordClass(Protocol): - @abstractmethod - def __call__(self, time: float, value: ST) -> Any: ... - - -RECORD_TUPLES = tuple[RECORD_FUNC, str] | tuple[type, str] - - -class ActiveStatus(Enum): - activating = "ACTIVATING" - deactivating = "DEACTIVATING" - - -def _compare(a: Any, b: Any) -> bool: - """Function for comparing any two objects. - - If an equality test fails, assume not equal. - - Args: - a (Any): Anything - b (Any): Also anything - - Returns: - bool: Are they the same - """ - try: - return cast(bool, a == b) - except Exception: - return False - - -class State(Generic[ST]): - """The particular condition that something is in at a specific time. - - The states are implemented as - `Descriptors `_ - which are associated to :class:`upstage.actor.Actor`. - - Note: - The classes that use this descriptor must contain an ``env`` attribute. - - States are aware - - """ - - def __init__( - self, - *, - default: ST | None = None, - frozen: bool = False, - no_init: bool = False, - valid_types: type | tuple[type, ...] | None = None, - recording: bool = False, - record_duplicates: bool = False, - default_factory: Callable[[], ST] | None = None, - allow_none_default: bool = False, - recording_functions: list[RECORD_TUPLES] | None = None, - ) -> None: - """Create a state descriptor for an Actor. - - The default can be set either with the value or the factory. Use the factory if - the default needs to be a list, dict, or similar type of object. The default - is used if both are present (not the factory). - - Setting frozen to True will throw an error if the value of the state is changed. - - The valid_types input will type-check when you initialize an actor. - - Recording enables logging the values of the state whenever they change, along - with the simulation time. This attempts to deepcopy the value. - - When a state is a mutable type, such as a dictionary or Counter, state - changes won't be recorded because the descriptor itself won't be modified - through the __set__ call. - - Args: - default (Any | None, optional): Default value of the state. Defaults to None. - frozen (bool, optional): If the state is allowed to change. Defaults to False. - no_init (bool, optional): Ignore the state in the init and rely on the default. - valid_types (type | tuple[type, ...] | None, optional): Types allowed. Defaults to None. - recording (bool, optional): If the state records itself. Defaults to False. - record_duplicates (bool, optional): If the state records duplicate values. - Defaults to False. - default_factory (Callable[[], type] | None, optional): Default from function. - Defaults to None. - allow_none_default (bool, optional): Consider a `None` default to be - valid - recording_functions (list[RECORD_TUPLES], optional): - A list of functions or callable classes to use when the state records. - The second entry in the tuple is a string of the name to use in - `_state_histories`. - """ - self._default = default - self._default_factory = default_factory - - if self._default is not None and self._default_factory is not None: - raise UpstageError("State needs to only use default or default factory.") - any_def = self._default is not None or self._default_factory is not None - - self._no_init = no_init - if self._no_init and not any_def: - raise SimulationError("State needs a default for no_init=True") - self._frozen = frozen - self._recording = recording - self._record_duplicates = record_duplicates - self._change_callbacks: dict[Any, CALLBACK_FUNC] = {} - self._allow_none_default = allow_none_default - self._recording_functions: list[tuple[RECORD_FUNC, str]] = [] - if recording_functions is not None: - for thing, name in recording_functions: - if isinstance(thing, type): - use = thing() - assert isinstance(use, RecordClass) - self._recording_functions.append((use, name)) - else: - self._recording_functions.append((thing, name)) - - self._types: tuple[type, ...] - - if isinstance(valid_types, type): - self._types = (valid_types,) - elif valid_types is None: - self._types = tuple() - else: - self._types = valid_types - self.IGNORE_LOCK: bool = False - - def _do_record_funcs(self, instance: "Actor", now: float, value: ST) -> None: - for func, name in self._recording_functions: - result = func(now, value) - new_append = (now, result) - if name not in instance._state_histories: - instance._state_histories[name] = [new_append] - elif self._record_duplicates or not _compare( - new_append, instance._state_histories[name][-1] - ): - instance._state_histories[name].append(new_append) - - def _do_record(self, instance: "Actor", value: ST, override: Any = None) -> None: - """Record the value of the state. - - Args: - instance (Actor): The actor holding the state - value (ST): State value - override (Any, optional): If given, record the override value - """ - if not self._recording: - return - if getattr(instance, "env", None) is None: - raise SimulationError( - f"Actor {instance} does not have an `env` attribute for state {self.name}" - ) - now = float(instance.env.now) - use = value if override is None else override - to_append = (now, deepcopy(use)) - if self.name not in instance._state_histories: - instance._state_histories[self.name] = [to_append] - elif self._record_duplicates or not _compare( - to_append, instance._state_histories[self.name][-1] - ): - instance._state_histories[self.name].append(to_append) - - self._do_record_funcs(instance, *to_append) - - def _do_callback(self, instance: "Actor", value: ST) -> None: - """Run callbacks for the state change. - - Args: - instance (Actor): The actor holding the state - value (Any): The value of the state - """ - for _, callback in self._change_callbacks.items(): - callback(instance, value) - - def _broadcast_change(self, instance: "Actor", name: str, value: ST) -> None: - """Send state change values to nucleus. - - Args: - instance (Actor): The actor holding the state - name (str): The state's name - value (Any): The state's value - """ - # broadcast changes to the instance - if instance._state_listener is not None: - instance._state_listener.send_change(name, value) - - # NOTE: A dictionary as a descriptor doesn't work well, - # because all the operations seem to happen *after* the get - # NOTE: Lists also have the same issue that - def _type_check(self, value: Any, throw: bool = True) -> bool: - """Check if a type matches this state.""" - if not self._types: - return True - ans = isinstance(value, self._types) - if throw and not ans: - raise TypeError(f"{value} is of type {type(value)} not of type {self._types}") - return ans - - def __set__(self, instance: "Actor", value: Any) -> None: - """Set the state's value. - - Args: - instance (Actor): The actor holding the state - value (Any): The state's value - """ - if self._frozen: - old_value = instance.__dict__.get(self.name, None) - if old_value is not None: - raise SimulationError( - f"State '{self}' on '{instance}' has already been frozen " - f"to value of {old_value}. It cannot be changed once set!" - ) - - self._type_check(value, throw=True) - - instance.__dict__[self.name] = value - - self._do_record(instance, value) - self._do_callback(instance, value) - - self._broadcast_change(instance, self.name, value) - - def __get__(self, instance: "Actor", objtype: type | None = None) -> ST: - if instance is None: - # instance attribute accessed on class, return self - return self # pragma: no cover - if self.name in instance._mimic_states: - actor, name = instance._mimic_states[self.name] - value = getattr(actor, name) - self.__set__(instance, value) - if self.name not in instance.__dict__: - raise SimulationError(f"State {self.name} should have been set.") - v = instance.__dict__[self.name] - return cast(ST, v) - - def __set_name__(self, owner: "Actor", name: str) -> None: - self.name = name - - def _set_default(self, instance: "Actor") -> None: - """Set the state's value on the actor the default. - - For allowed None default, skip setting it. This will - error on the get, which is expected. - - Args: - instance (Actor): Actor holding the state. - """ - # The default sits on the descriptor class, not on the instance. - # If there's a factory we need to remake the default - if self._default_factory is not None: - value = self._default_factory() - self.__set__(instance, value) - return - if self._default is None: - if self._allow_none_default: - return - raise SimulationError(f"State {self.name} not allowed `None` default.") - self.__set__(instance, self._default) - - def has_default(self) -> bool: - """Check if a default exists. - - Returns: - bool - """ - if self._allow_none_default: - return True - return self._default is not None or self._default_factory is not None - - def _add_callback(self, source: Any, callback: CALLBACK_FUNC) -> None: - """Add a recording callback. - - Args: - source (Any): A key for the callback - callback (Callable[[Actor, Any], None]): A function to call - """ - self._change_callbacks[source] = callback - - def _remove_callback(self, source: Any) -> None: - """Remove a callback. - - Args: - source (Any): The callback's key - """ - del self._change_callbacks[source] - - @property - def is_recording(self) -> bool: - """Check if the state is recording. - - Returns: - bool - """ - return self._recording - - -class DetectabilityState(State[bool]): - """A state whose purpose is to indicate True or False. - - For consideration in the motion manager's <>LocationChangingState checks. - """ - - def __init__(self, *, default: bool = False, recording: bool = False) -> None: - """Create the detectability state. - - Args: - default (bool, optional): If the state starts on/off. Defaults to False. - recording (bool, optional): If the state records. Defaults to False. - """ - super().__init__( - default=default, - frozen=False, - valid_types=(bool,), - recording=recording, - ) - - def __set__(self, instance: "Actor", value: bool) -> None: - """Set the detectability. - - Args: - instance (Actor): The actor - value (bool): The value to set - """ - # Setting the default value shouldn't trigger the callback - # to the motion manager. - was_set = True - if self.name not in instance.__dict__: - was_set = False - super().__set__(instance, value) - if hasattr(instance.stage, "motion_manager") and was_set: - mgr = instance.stage.motion_manager - if not value: - mgr._mover_not_detectable(instance) - else: - mgr._mover_became_detectable(instance) - - -class ActiveState(State, Generic[ST]): - """Base class for states that change over time according to some rules. - - This class must be subclasses with an implemented `active` method. - - """ - - def _active(self, instance: "Actor") -> Any: - """Determine if the instance has an active state. - - Note: - The instance must have two methods: ``get_active_state_data`` and - ``_set_active_state_data``. - - Note: - When you call ``activate_state`` from an actor, that is where - you define the activity data. It is up to the Actor's subclass to - make sure the activity data meet its needs. - - The first entry in the active data is always the time. - Alternatively, you can call ``self.get_activity_data`` for some - more data. - - """ - raise NotImplementedError("Method active not implemented.") - - def __get__(self, instance: "Actor", owner: type | None = None) -> ST: - if instance is None: - # instance attribute accessed on class, return self - return self # pragma: no cover - if self.name in instance._mimic_states: - actor, name = instance._mimic_states[self.name] - value = getattr(actor, name) - self.__set__(instance, value) - return cast(ST, value) - # test if this instance is active or not - res = self._active(instance) - # comes back as None (not active), or if it can be obtained from dict - if res is None: - res = instance.__dict__[self.name] - return cast(ST, res) - - def get_activity_data(self, instance: "Actor") -> dict[str, Any]: - """Get the data useful for updating active states. - - Returns: - dict[str, Any]: A dictionary with the state's pertinent data. Includes the actor's - environment current time (``'now'``) and the value of the actor's - state (``'state'``). - - """ - res = instance.get_active_state_data(self.name, without_update=True) - res["now"] = instance.env.now - res["value"] = instance.__dict__[self.name] - return res - - def activate(self, instance: "Actor", task: Task | None = None) -> None: - """Method to run when a state is activated. - - Used to help record the right data about the active state. - - Use this with __super__ for motion states to deactivate their motion from - the motion manager. - """ - self._do_record(instance, None, override=ActiveStatus.activating) - - def deactivate(self, instance: "Actor", task: Task | None = None) -> bool: - """Optional method to override that is called when a state is deactivated. - - Useful for motion states to deactivate their motion from - the motion manager. - - Defaults to any deactivation removing active state data. - """ - # Returns if the state should be ignored - # A False means the state is completely deactivated - self._do_record(instance, None, override=ActiveStatus.deactivating) - return False - - -class LinearChangingState(ActiveState[float]): - """A state whose value changes linearly over time. - - When activating: - - >>> class Lin(Actor): - >>> x = LinearChangingState() - >>> - >>> def task(self, actor: Lin): - >>> actor.activate_state( - >>> name="x", - >>> task=self, - >>> rate=3.2, - >>> ) - """ - - def _active(self, instance: "Actor") -> float | None: - """Return a value to set based on time or some other criteria.""" - data = self.get_activity_data(instance) - now: float = data["now"] - current: float = data["value"] - started: float | None = data.get("started_at", None) - if started is None: - # it's not currently active - return None - # The user needs to know what their active data looks like. - # Alternatively, it could be defined in the state or the actor. - rate: float = data["rate"] - if now < started: - raise SimulationError( - f"Cannot set state '{self.name}' start time after now. " - f"This probably happened because the active state was " - f"set incorrectly." - ) - value = (now - started) * rate - return_value = current + value - self.__set__(instance, return_value) - instance._set_active_state_data( - state_name=self.name, - started_at=now, - rate=rate, - ) - return return_value - - -class CartesianLocationChangingState(ActiveState[CartesianLocation]): - """A state that contains the location in 3-dimensional Cartesian space. - - Movement is along straight lines in that space. - - For activating: - >>> actor.activate_state( - >>> state=, - >>> task=self, # usually - >>> speed=, - >>> waypoints=[ - >>> List of CartesianLocation - >>> ] - >>> ) - """ - - def __init__(self, *, recording: bool = False): - """Set a Location changing state. - - Defaults are disabled due to immutability of location objects. - (We could copy it, but it seems like better practice to force inputting it at runtime.) - - Args: - recording (bool, optional): Whether to record. Defaults to False. - """ - super().__init__( - default=None, - frozen=False, - default_factory=None, - valid_types=(CartesianLocation,), - recording=recording, - ) - - def _setup(self, instance: "Actor") -> None: - """Initialize data about a path. - - Args: - instance (Actor): The actor - """ - data = self.get_activity_data(instance) - current: CartesianLocation = data["value"] - speed: float = data["speed"] - waypoints: list[CartesianLocation] = data["waypoints"] - # get the times, distances, and bearings from the waypoints - times: list[float] = [] - distances: list[float] = [] - starts: list[CartesianLocation] = [] - vectors: list[list[float]] = [] - for wypt in waypoints: - dist = wypt - current - time = dist / speed - times.append(time) - distances.append(dist) - starts.append(current.copy()) - vectors.append(_vector_subtract(wypt._as_array(), current._as_array())) - current = wypt - - path_data = { - "times": times, - "distances": distances, - "starts": starts, - "vectors": vectors, - } - instance._set_active_state_data( - self.name, - started_at=data["now"], - origin=data["value"], - speed=speed, - waypoints=waypoints, - path_data=path_data, - ) - # if there is a motion manager, notify it - if hasattr(instance.stage, "motion_manager"): - if not getattr(instance, "_is_rehearsing", False): - instance.stage.motion_manager._start_mover( - instance, - speed, - [data["value"]] + waypoints, - ) - - def _get_index(self, path_data: dict[str, Any], time_elapsed: float) -> tuple[int, float]: - """Find out how far along waypoints the state is. - - Args: - path_data (dict[str, Any]): Data about the movement path - time_elapsed (float): Time spent moving - - Returns: - int: index in waypoints - float: time spent on path - """ - sum_t = 0.0 - t: float - for i, t in enumerate(path_data["times"]): - sum_t += t - if time_elapsed <= (sum_t + 1e-12): - return i, sum_t - t - raise SimulationError( - "CartesianLocation active state exceeded travel time: " - f"elapsed: {time_elapsed}, maximum: {sum_t}" - ) - - def _get_remaining_waypoints(self, instance: "Actor") -> list[CartesianLocation]: - """Convenience for getting waypoints left. - - Args: - instance (Actor): The owning actor. - - Returns: - list[CartesianLocation]: The waypoints left - """ - data = self.get_activity_data(instance) - current_time: float = data["now"] - path_start_time: float = data["started_at"] - elapsed = current_time - path_start_time - idx, _ = self._get_index(data["path_data"], elapsed) - return list(data["waypoints"][idx:]) - - def _active(self, instance: "Actor") -> CartesianLocation | None: - """Get the current value while active. - - Args: - instance (Actor): The owning actor - - Returns: - CartesianLocation | None: The current value - """ - data = self.get_activity_data(instance) - path_start_time: float | None = data.get("started_at", None) - if path_start_time is None: - # it's not active - return None - - path_data: dict[str, Any] | None = data.get("path_data", None) - if path_data is None: - self._setup(instance) - data = self.get_activity_data(instance) - - path_data: dict[str, Any] = data["path_data"] - current_time: float = data["now"] - elapsed = current_time - path_start_time - if elapsed < 0: - # Can probably only happen if active state is set incorrectly - raise SimulationError(f"Cannot set state '{self.name}' start time in the future!") - elif elapsed == 0: - return_value: CartesianLocation = data["value"] # pragma: no cover - else: - # Get the location along the waypoint path - wypt_index, wypt_start = self._get_index(path_data, elapsed) - time_along = elapsed - wypt_start - path_time: float = path_data["times"][wypt_index] - path_start: CartesianLocation = path_data["starts"][wypt_index] - path_vector: list[float] = path_data["vectors"][wypt_index] - time_frac = time_along / path_time - direction_amount = [time_frac * v for v in path_vector] - new_point = _vector_add(path_start._as_array(), direction_amount) - - # make the right kind of location object - new_location = CartesianLocation( - x=new_point[0], - y=new_point[1], - z=new_point[2], - ) - return_value = new_location - - self.__set__(instance, return_value) - - # No new data needs to be added - # Only the current time is needed once we run _setup() - data["value"] = return_value - instance._set_active_state_data( - state_name=self.name, - **data, - ) - - return return_value - - def deactivate(self, instance: "Actor", task: Task | None = None) -> bool: - """Deactivate the motion. - - Args: - instance (Actor): The owning actor - task (Task): The task calling the deactivation. - - Returns: - bool: _description_ - """ - if hasattr(instance.stage, "motion_manager"): - if not getattr(instance, "_is_rehearsing", False): - instance.stage.motion_manager._stop_mover(instance) - return super().deactivate(instance, task) - - -class GeodeticLocationChangingState(ActiveState[GeodeticLocation]): - """A state that contains a location around an ellipsoid that follows great-circle paths. - - Requires a distance model class that implements: - 1. distance_and_bearing - 2. point_from_bearing_dist - and outputs objects with .lat and .lon attributes - - - For activating: - - >>> actor.activate_state( - >>> state=, - >>> task=self, # usually - >>> speed=, - >>> waypoints=[ - >>> List of CartesianLocation - >>> ] - >>> ) - """ - - def __init__(self, *, recording: bool = False) -> None: - """Create the location changing state. - - Defaults are disabled due to immutability of location objects. - (We could copy it, but it seems like better practice to force inputting it at runtime.) - - Args: - recording (bool, optional): If the location is recorded. Defaults to False. - """ - super().__init__( - default=None, - frozen=False, - valid_types=(GeodeticLocation,), - recording=recording, - ) - - def _setup(self, instance: "Actor") -> None: - """Initialize data about a path.""" - STAGE = instance.stage - data = self.get_activity_data(instance) - current: GeodeticLocation = data["value"] - speed: float = data["speed"] - waypoints: list[GeodeticLocation] = data["waypoints"] - # get the times, distances, and bearings from the waypoints - times: list[float] = [] - distances: list[float] = [] - bearings: list[float] = [] - starts: list[GeodeticLocation] = [] - for wypt in waypoints: - dist, bear = STAGE.stage_model.distance_and_bearing( - (current.lat, current.lon), - (wypt.lat, wypt.lon), - units=STAGE.distance_units, - ) - time = dist / speed - times.append(time) - distances.append(dist) - bearings.append(bear) - starts.append(current.copy()) - current = wypt - - path_data = { - "times": times, - "distances": distances, - "bearings": bearings, - "starts": starts, - } - instance._set_active_state_data( - self.name, - started_at=data["now"], - origin=data["value"], - speed=speed, - waypoints=waypoints, - path_data=path_data, - ) - - # if there is a motion manager, notify it - if hasattr(STAGE, "motion_manager"): - if not getattr(instance, "_is_rehearsing", False): - STAGE.motion_manager._start_mover( - instance, - speed, - [data["value"]] + waypoints, - ) - - def _get_index(self, path_data: dict[str, Any], time_elapsed: float) -> tuple[int, float]: - """Get the index of the waypoint the path is on. - - Args: - path_data (dict[str, Any]): Data about the motion - time_elapsed (float): Time spent on motion - - Returns: - int: Index of the waypoint - float: time elapsed - """ - sum_t = 0.0 - t: float - for i, t in enumerate(path_data["times"]): - sum_t += t - if time_elapsed <= (sum_t + 1e-4): # near one second allowed - return i, sum_t - t - raise SimulationError( - f"GeodeticLocation active state exceeded travel time: Elapsed: {time_elapsed}, " - "Actual: {sum_t}" - ) - - def _get_remaining_waypoints(self, instance: "Actor") -> list[GeodeticLocation]: - """Get waypoints left in travel. - - Args: - instance (Actor): The owning actor. - - Returns: - list[GeodeticLocation]: Waypoint remaining - """ - data = self.get_activity_data(instance) - current_time: float = data["now"] - path_start_time: float = data["started_at"] - elapsed = current_time - path_start_time - idx, _ = self._get_index(data["path_data"], elapsed) - wypts: list[GeodeticLocation] = data["waypoints"] - return wypts[idx:] - - def _active(self, instance: "Actor") -> GeodeticLocation | None: - """Get the value of the location while in motion. - - Args: - instance (Actor): The owning actor. - - Returns: - GeodeticLocation | None: Location while in motion. None if still. - """ - STAGE = instance.stage - data = self.get_activity_data(instance) - path_start_time: float | None = data.get("started_at", None) - if path_start_time is None: - # it's not active - return None - - path_data: dict[str, Any] | None = data.get("path_data", None) - if path_data is None: - self._setup(instance) - data = self.get_activity_data(instance) - - path_data: dict[str, Any] = data["path_data"] - - current_time: float = data["now"] - path_start_time: float = data["started_at"] - elapsed = current_time - path_start_time - - if elapsed < 0: - # Can probably only happen if active state is set incorrectly - raise SimulationError(f"Cannot set state '{self.name}' start time in the future!") - elif elapsed == 0: - return_value: GeodeticLocation = data["value"] # pragma: no cover - else: - # Get the location along the waypoint path - wypt_index, wypt_start = self._get_index(path_data, elapsed) - time_along = elapsed - wypt_start - path_time: float = path_data["times"][wypt_index] - path_dist: float = path_data["distances"][wypt_index] - path_bearing: float = path_data["bearings"][wypt_index] - path_start: GeodeticLocation = path_data["starts"][wypt_index] - moved_distance = (time_along / path_time) * path_dist - new_point = STAGE.stage_model.point_from_bearing_dist( - (path_start.lat, path_start.lon), - path_bearing, - moved_distance, - STAGE.distance_units, - ) - # update the altitude - waypoint: GeodeticLocation = data["waypoints"][wypt_index] - alt_shift = waypoint.alt - path_start.alt - alt_shift *= time_along / path_time - new_alt = path_start.alt + alt_shift - # make the right kind of location object - lat, lon = new_point[0], new_point[1] - new_location = GeodeticLocation( - lat, - lon, - new_alt, - ) - return_value = new_location - - self.__set__(instance, return_value) - - # No new data needs to be added - # Only the current time is needed once we run _setup() - instance._set_active_state_data( - state_name=self.name, - **data, - ) - - return return_value - - def deactivate(self, instance: "Actor", task: Task | None = None) -> bool: - """Deactivate the state. - - Args: - instance (Actor): The owning actor - task (Task): The task doing the deactivating - - Returns: - bool: If the state is all done - """ - STAGE = instance.stage - if hasattr(STAGE, "motion_manager"): - if not getattr(instance, "_is_rehearsing", False): - STAGE.motion_manager._stop_mover(instance) - return super().deactivate(instance, task) - - -T = TypeVar("T", bound=Store | Container) - - -class ResourceState(State, Generic[T]): - """A State class for States that are meant to be Stores or Containers. - - This should enable easier initialization of Actors with stores/containers or - similar objects as states. - - No input is needed for the state if you define a default resource class in - the class definition and do not wish to modify the default inputs of that - class. You can also define default inputs for the resource instantiation. - - The input an Actor needs to receive for a ResourceState is a dictionary of: - * 'kind': (optional if you provided a default) - * 'capacity': (optional, works on stores and containers) - * 'init': (optional, works on containers) - * key:value for any other input expected as a keyword argument by the resource class - - Note that the resource class given must accept the environment as the first - positional argument. This is to maintain compatibility with simpy. - - Example: - >>> class Warehouse(Actor): - >>> shelf = ResourceState[Store](default=Store) - >>> bucket = ResourceState[Container]( - >>> default=Container, - >>> valid_types=(Container, SelfMonitoringContainer), - >>> ) - >>> charger = ResourceState[Store]( - >>> default=Store, - >>> default_kwargs={"capacity": 5}, - >>> ) - >>> - >>> wh = Warehouse( - >>> name='Depot', - >>> shelf={'capacity': 10}, - >>> bucket={'kind': SelfMonitoringContainer, 'init': 30}, - >>> ) - """ - - def __init__( - self, - *, - default: Any | None = None, - valid_types: type | tuple[type, ...] | None = None, - default_kwargs: dict[str, Any] | None = None, - ) -> None: - """Create a resource State decorator. - - Args: - default (Any | None, optional): Default store/container class. Defaults to None. - valid_types (type | tuple[type, ...] | None, optional): Valid store/container - classes. Defaults to None. - default_kwargs (dict[str, Any], optional): Kwargs to pass to the creation - of the default store/container class. - """ - if isinstance(valid_types, type): - valid_types = (valid_types,) - - if valid_types: - for v in valid_types: - if not isinstance(v, type) or not issubclass(v, Store | Container): - raise UpstageError(f"Bad valid type for {self}: {v}") - else: - valid_types = (Store, Container) - - if default is not None and ( - not isinstance(default, type) or not issubclass(default, Store | Container) - ): - raise UpstageError(f"Bad default type for {self}: {default}") - - super().__init__( - default=default, - frozen=False, - recording=False, - valid_types=valid_types, - ) - self._default_kwargs = default_kwargs.copy() if default_kwargs is not None else {} - self._been_set: set[Actor] = set() - - def __set__(self, instance: "Actor", value: dict | Any) -> None: - """Set the state value. - - Args: - instance (Actor): The actor instance - value (dict | Any): Either a dictionary of resource data OR an actual resource - """ - if instance in self._been_set: - raise UpstageError( - f"State '{self}' on '{instance}' has already been created " - "It cannot be changed once set!" - ) - - if not isinstance(value, dict): - # we've been passed an actual resource, so save it and leave - if not isinstance(value, self._types): - raise UpstageError(f"Resource object: '{value}' is not an expected type.") - instance.__dict__[self.name] = value - self._been_set.add(instance) - return - - resource_type = value.get("kind", self._default) - if resource_type is None: - raise UpstageError(f"No resource type (Store, e.g.) specified for {instance}") - - if self._types and not issubclass(resource_type, self._types): - raise UpstageError( - f"{resource_type} is of type {type(resource_type)} not of type {self._types}" - ) - - env = getattr(instance, "env", None) - if env is None: - raise UpstageError( - f"Actor {instance} does not have an `env` attribute for state {self.name}" - ) - kwargs = self._default_kwargs.copy() - kwargs.update({k: v for k, v in value.items() if k != "kind"}) - try: - resource_obj = resource_type(env, **kwargs) - except TypeError as e: - raise UpstageError( - f"Bad argument input to resource state {self.name}" - f" resource class {resource_type} :{e}" - ) - except Exception as e: - raise UpstageError(f"Exception in ResourceState init: {e}") - - instance.__dict__[self.name] = resource_obj - self._been_set.add(instance) - # remember what we did for cloning - instance.__dict__["_memory_for_" + self.name] = kwargs.copy() - - self._broadcast_change(instance, self.name, value) - - def _set_default(self, instance: "Actor") -> None: - """Set the default conditions. - - The empty dictionary input forces default to happen the right way. - - Args: - instance (Actor): The actor holding this state. - """ - self.__set__(instance, {}) - - def __get__(self, instance: "Actor", owner: type | None = None) -> T: - if instance is None: - # instance attribute accessed on class, return self - return self # pragma: no cover - if self.name not in instance.__dict__: - self._set_default(instance) - obj = instance.__dict__[self.name] - if not issubclass(type(obj), Store | Container): - raise UpstageError("Bad type of ResourceStatee") - return cast(T, obj) - - def _make_clone(self, instance: "Actor", copy: T) -> T: - """Method to support cloning a store or container. - - Args: - instance (Actor): The owning actor - copy (T): The store or container to copy - - Returns: - T: The copied store or container - """ - base_class = type(copy) - memory: dict[str, Any] = instance.__dict__[f"_memory_for_{self.name}"] - new = base_class(instance.env, **memory) # type: ignore [arg-type] - if isinstance(copy, Store) and isinstance(new, Store): - new.items = list(copy.items) - if isinstance(copy, Container) and isinstance(new, Container): - # This is a particularity of simpy containers - new._level = float(copy.level) - return cast(T, new) - - -class CommunicationStore(ResourceState[Store]): - """A State class for communications inputs. - - Used for automated finding of communication inputs on Actors by the CommsTransfer code. - - Follows the same rules for defaults as `ResourceState`, except this - defaults to a SelfMonitoringStore without any user input. - - Only resources inheriting from simpy.Store will work for this state. - Capacities are assumed infinite. - - The input an Actor needs to receive for a CommunicationStore is a dictionary of: - >>> { - >>> 'kind': (optional) - >>> 'modes': (optional) - >>> } - - Example: - >>> class Worker(Actor): - >>> walkie = CommunicationStore(modes="UHF") - >>> intercom = CommunicationStore(modes=None) - >>> - >>> worker = Worker( - >>> name='Billy', - >>> walkie={'kind': SelfMonitoringStore}, - >>> intercom={"modes": "loudspeaker"}, - >>> ) - - """ - - def __init__( - self, - *, - modes: str | list[str] | None, - default: type | None = None, - valid_types: type | tuple[type, ...] | None = None, - ): - """Create a comms store. - - Args: - modes (str, list[str], optional): Modes to describe the comms channel. - default (type | None, optional): Store class by default. - Defaults to None. - valid_types (type | tuple[type, ...] | None, optional): Valid store classes. - Defaults to None. - """ - if default is None: - default = SelfMonitoringStore - if valid_types is None: - valid_types = (Store, SelfMonitoringStore) - elif isinstance(valid_types, type): - valid_types = (valid_types,) - for v in valid_types: - if not issubclass(v, Store): - raise SimulationError("CommunicationStore must use a Store subclass") - super().__init__(default=default, valid_types=valid_types) - self._modes = modes - - @property - def _modename(self) -> str: - return "_" + self.name + "__mode_names_" - - def __set__(self, instance: "Actor", value: dict | Any) -> None: - # See if the instance has any specific mode data to find. - modes: str | list[str] | None = self._modes - if isinstance(value, dict) and "modes" in value: - modes = value.pop("modes") - super().__set__(instance, value) - if modes is None: - raise SimulationError( - f"CommunicationsStore {self.name} needs a mode defined" - " by default or through the initialization." - ) - if isinstance(modes, str): - modes = [modes] - if not isinstance(modes, list) and not all(isinstance(x, str) for x in modes): - raise SimulationError("CommunicationsStore modes should be a list of strings.") - instance.__dict__[self._modename] = set(modes) - - -class _KeyValueBase(State): - """A base state for holding key/value pairs. - - This is greatly simplified state meant for a runtime - definition, rather than assuming defaults. - - Use either a dataclass or a dictionary for the value of - the state when instantiating the Actor. - """ - - def __init__( - self, - *, - valid_types: type | tuple[type, ...] | None = None, - recording: bool = False, - record_duplicates: bool = False, - recording_functions: list[RECORD_TUPLES] | None = None, - ) -> None: - # Frozen, recording, and record duplicates are set so - # that the overall state's value (a dictionary) is - # never touched, and only its members are accessed. - super().__init__( - frozen=True, - valid_types=valid_types, - recording=False, - record_duplicates=False, - recording_functions=recording_functions, - ) - self._record_indiv = recording - self._record_indiv_dupe = record_duplicates - - def _record_single(self, instance: "Actor", time: float, key: str, value: Any) -> None: - name = f"{self.name}.{key}" - new = (time, value) - if name not in instance._state_histories: - instance._state_histories[name] = [new] - elif self._record_duplicates or not _compare(new, instance._state_histories[name][-1]): - instance._state_histories[name].append(new) - - def _get_keys_values(self, instance: "Actor") -> list[tuple[str, Any]]: - raise NotImplementedError() - - def _get_value(self, instance: "Actor", key: str) -> Any: - raise NotImplementedError - - def _record_state(self, instance: "Actor", key: str | None = None, all: bool = False) -> None: - if not self._record_indiv: - return - - if getattr(instance, "env", None) is None: - raise SimulationError( - f"Actor {instance} does not have an `env` attribute for state {self.name}" - ) - now = float(instance.env.now) - if all: - for k, v in self._get_keys_values(instance): - self._record_single(instance, now, k, v) - elif key is not None: - v = self._get_value(instance, key) - self._record_single(instance, now, key, v) - else: - raise SimulationError(f"No key given for recording on {instance}") - - self._do_record_funcs(instance, now, instance.__dict__[self.name]) - - def _make_clone(self, instance: "Actor") -> Any: - raise NotImplementedError() - - -VT = TypeVar("VT") - - -class DictionaryState(_KeyValueBase, Generic[VT]): - """A state that contains a {str: value} dictionary. - - This state provides features for holding a dictionary that is self-recording - when attributes are set. Similar to States, recording functions can augment - the recorded information on every key/value update. Recorded keys are - given the variable name of . - - For simplicity of data recording, this state expects all keys to be strings. - If you supply a valid_type input, the state will type check your values - against it. - - The dictionary state does not expect any default factories or settings, you - must initialize it with at least a blank dictionary. - - The state is not actually a dictionary, but a proxy for the dictionary where - get, set, contains, and iter operations are supported. - - Example: - - .. code-block:: python - - class VendingMachine(UP.Actor): - inventory = UP.DictionaryState[int](valid_types=(int,), recording=True) - requested = UP.DictionaryState[int](valid_types=(int,), recording=True) - request = UP.ResourceState[Store](default=Store) - - class TrackRequests(UP.Task): - def task(self, *, actor: VendingMachine) -> TASK_GEN: - get = UP.Get(actor.request) - yield get - item_name = get.get_value() - actor.requested.setdefault(item_name, 0) - actor.requested[item_name] += 1 - if item_name in actor.inventory: - actor.inventory[item_name] -= 1 - else: - print(f"We have no items named {item_name}".) - - inventory = {"chips": 10, "cookies":10, "granola bars": 24} - with UP.EnvironmentContext() as env: - vend = VendingMachine( - name="Machine", - inventory=inventory, - request={}, - ) - ... - - - Args: - Generic (_type_): The type information for the values - """ - - def __get__(self, instance: "Actor", objtype: type | None = None) -> dict[str, VT]: - return cast( - dict[str, VT], _DictionaryProxy[VT](self, instance, instance.__dict__[self.name]) - ) - - def __set__(self, instance: "Actor", value: dict[str, VT]) -> None: - if self.name in instance.__dict__: - raise SimulationError(f"State {self.name} already set on {instance}.") - - instance.__dict__[self.name] = {} - for k, v in value.items(): - self._type_check(v, throw=True) - instance.__dict__[self.name][k] = v - - self._record_state(instance, all=True) - - def _get_keys_values(self, instance: "Actor") -> list[tuple[str, Any]]: - return list(instance.__dict__[self.name].items()) - - def _get_value(self, instance: "Actor", key: str) -> Any: - return instance.__dict__[self.name][key] - - def _make_clone(self, instance: "Actor") -> dict[str, VT]: - return cast(dict[str, VT], instance.__dict__[self.name].copy()) - - -DCT = TypeVar("DCT") - - -class DataclassState(_KeyValueBase, Generic[DCT]): - """A state that contains a dataclass. - - This state provides features for holding a dataclass that is self-recording - when attributes are set. Similar to States, recording functions can augment - the recorded information on every key/value update. Recorded keys are - given the variable name of . - - If you supply a dataclass object to the valid_type input, the state will - type check your values against it. These states are less flexible than - dictionary states, but do provide more specific type information on a per- - attribute basis. If you have common data structures that you might update - frequently, a DataclassState can provide some structure that is more flexible - than changing an actor's states. It also allows other models to be incorporated - that are dependent on data only, and not the actors themselves. - - The dataclass state does not expect any default factories or settings, you - must initialize it with a dataclass instance. - - The state is not actually a dataclass, but a proxy for the dataclass where - get, set, contains, and iter operations are supported. - - Example: - - .. code-block:: python - - @dataclass - class Properties: - speed: float - health: float - damage: float - armor: float = field(default=0.0) - - class Barbarian(UP.Actor): - properties = UP.DataclassState[Properties]( - valid_types=(Properties,), - recording=True, - ) - - def fight_model(fighter1: Properties, fighter2: Properties): - ... - - class ArenaBattle(UP.Task): - def task(self, *, actor: ArenaActor) -> TASK_GEN: - get = UP.Get(actor.next_fight) - yield get - b1: Barbarian - b2: Barbarian - b1, b2 = get.get_value() - winner = fight_model(b1.properties, b2.properties) - - Args: - Generic (_type_): The type information for the values - """ - - def __get__(self, instance: "Actor", objtype: type | None = None) -> DCT: - return cast(DCT, _DataclassProxy[DCT](self, instance, instance.__dict__[self.name])) - - def __set__(self, instance: "Actor", value: DCT) -> None: - if self.name in instance.__dict__: - raise SimulationError(f"State {self.name} already set on {instance}.") - - self._type_check(value, throw=True) - instance.__dict__[self.name] = value - - self._record_state(instance, all=True) - - def _get_keys_values(self, instance: "Actor") -> list[tuple[str, Any]]: - ans = [] - dc = instance.__dict__[self.name] - for field in fields(dc): - ans.append((field.name, getattr(dc, field.name))) - return ans - - def _get_value(self, instance: "Actor", key: str) -> Any: - return getattr(instance.__dict__[self.name], key) - - def _make_clone(self, instance: "Actor") -> DCT: - return cast(DCT, replace(instance.__dict__[self.name])) - - -class MultiStoreState(DictionaryState[T]): - """A state for holding stores or containers keyed by a string. - - Works best when all values are the same kind of container. - - This state follows rules similar to ResourceState, but for a dictionary - of container/store objects. - - The input an Actor needs to receive for a MultiStoreState is a dictionary of: - * 'kind': (optional if you provided a default) - * 'capacity': (optional, works on stores and containers) - * 'init': (optional, works on containers) - * key:value for any other input expected as a keyword argument by the resource class - - Types are enforced via `valid_types`, and if no `kind` is specified, the - `default` input is used. - - The default kwargs will be applied to any input, so make sure they are - compatible with different containers, or be more speficific with your typing. - Arguments that don't apply will raise an error. - - Example: - - .. code-block:: python - - class Warehouse(Actor): - storage = MultiStoreState[Store| Container]( - default=Store, - valid_types=(Store, Container), - default_kwargs={"capacity": 100}, - ) - - wh = Warehouse( - name='Depot', - storage = { - "shelf":{"capacity":10}, - "bucket":{"kind": SelfMonitoringContainer, "init": 30}, - "charger":{}, - } - ) - wh.storage["shelf"].capacity == 10 - wh.storage["bucket"].level == 30 - wh.storage["charger"].capacity == 100 - wh.storage["charger"].items == [] - - - """ - - def __init__( - self, - *, - valid_types: type | tuple[type, ...] | None = None, - default: type[Store] | type[Container] | None = None, - default_kwargs: dict[str, Any] | None = None, - ) -> None: - super().__init__(valid_types=valid_types) - self._default = default - self._default_kwargs = {} if default_kwargs is None else default_kwargs.copy() - - def _type_checker(self, resource_type: Any) -> None: - any_type = False - for t in self._types: - if not (issubclass(t, Store) or issubclass(t, Container)): - raise UpstageError("Bad valid_type for MultiStoreState.") - inst = isinstance(resource_type, t) - typ = issubclass(resource_type, t) if type(resource_type) is type else True - if inst or typ: - any_type = True - if self._types and not any_type: - raise UpstageError( - f"{resource_type} is of type {resource_type} not of type {self._types}" - ) - - def _make_resource(self, env: Environment, input: T | dict[str, Any]) -> T: - if not isinstance(input, dict): - # we've been passed an actual resource, so save it and leave - self._type_checker(input) - return input - - kwargs = self._default_kwargs.copy() - kwargs.update({k: v for k, v in input.items() if k != "kind"}) - - resource_type = input.get("kind", self._default) - if resource_type is None: - raise UpstageError("No default specified for MultiStoreState. Did you forget 'kind'?") - self._type_checker(resource_type) - try: - resource_obj = resource_type(env, **kwargs) - except TypeError as e: - raise UpstageError( - f"Bad argument input to resource state {self.name}" - f" resource class {resource_type} :{e}" - ) - except Exception as e: - raise UpstageError(f"Exception in ResourceState init: {e}") - return cast(T, resource_obj) - - def __set__( - self, instance: "Actor", value: dict[str, T | dict[str, Any]] | Iterable[str] - ) -> None: - if self.name in instance.__dict__: - raise SimulationError(f"State {self.name} already set on {instance}.") - env = getattr(instance, "env", None) - if env is None or not isinstance(env, Environment): - raise UpstageError( - f"Actor {instance} does not have the right `env` attribute for state {self.name}" - ) - # process the values - use: dict[str, T] = {} - if not isinstance(value, dict): - value = {name: {} for name in value} - attrs: dict[str, Any] | T - for name, attrs in value.items(): - use[name] = self._make_resource(env, attrs) - - super().__set__(instance, use) - - def has_default(self) -> bool: - # Force the state to be defined. - return False diff --git a/src/upstage_des/task.py b/src/upstage_des/task.py deleted file mode 100644 index 158e30a..0000000 --- a/src/upstage_des/task.py +++ /dev/null @@ -1,679 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Tasks constitute the actions that Actors can perform.""" - -from collections.abc import Callable, Generator, Iterable -from enum import IntFlag -from functools import wraps -from typing import TYPE_CHECKING, Any, TypeVar -from warnings import warn - -from simpy import Environment as SimpyEnv -from simpy import Event as SimpyEvent -from simpy import Interrupt, Process - -if TYPE_CHECKING: - from .actor import Actor - from .task_network import TaskNetwork - -from .base import ENV_CONTEXT_VAR, MockEnvironment, SettableEnv, SimulationError -from .constants import PLANNING_FACTOR_OBJECT -from .events import BaseEvent, Event -from .routines import Routine - -__all__ = ("DecisionTask", "Task", "process", "TerminalTask", "InterruptStates") - - -NOT_IMPLEMENTED_MSG = "User must define the actions performed when executing this task" - -TASK_GEN = Generator[BaseEvent | Routine, Any, None] - - -class InterruptStates(IntFlag): - """Class that describes how to behave after an interrupt.""" - - END = 0 - IGNORE = 1 - RESTART = 2 - - -def process( - func: Callable[..., Generator[SimpyEvent, Any, None]], -) -> Callable[..., Process]: - """Decorate a ``simpy`` process to schedule it as a callable. - - Allows users to decorate a generator, and when they want to schedule them - as a ``simpy`` process, they can simply call it, e.g., instead of calling: - - Usage: - - >>> from upstage.api import process, Wait - ... - >>> @process - >>> def generator(wait_period=1.0, msg="Finished Waiting"): - >>> # A simple process that periodically prints a statement - >>> while True: - >>> yield Wait(wait_period).as_event() - >>> print(msg) - ... - >>> @process - >>> def another_process(): - >>> # Some other process that calls the first one - >>> generator() - - Args: - func (Callable[..., Generator[BaseEvent, None, None]]): The process function that is a - generator of simpy events. - - Returns: - Process: The generator as a ``simpy`` process. - - Note: - The value of this decorator is that it reduces the chance of a user - forgetting to call the generator as a process, which tends to produce - behaviors that are difficult to troubleshoot because the code will - build and can run, but the simulation will not work schedule the - process defined by the generator. - - """ - - @wraps(func) - def wrapped_generator(*args: Any, **kwargs: Any) -> Process: - """Wrap the generator with a function that calls it as a process.""" - try: - environment = ENV_CONTEXT_VAR.get() - except LookupError: - raise SimulationError("No environment found on process call") - return environment.process(func(*args, **kwargs)) - - return wrapped_generator - - -EVT = TypeVar("EVT", bound=BaseEvent) -REH_ACTOR = TypeVar("REH_ACTOR", bound="Actor") - - -class Task(SettableEnv): - """A Task is an action that can be performed by an Actor.""" - - INTERRUPT = InterruptStates - - def __init__(self) -> None: - """Create a task instance.""" - super().__init__() - self._network_name: str | None = None - self._network_ref: TaskNetwork | None = None - self._marker: str | None = None - self._marked_time: float | None = None - self._interrupt_action: InterruptStates = InterruptStates.END - self._rehearsing: bool = False - - def task(self, *, actor: Any) -> TASK_GEN: - """Define the process this task follows.""" - raise NotImplementedError(NOT_IMPLEMENTED_MSG) - - def on_interrupt(self, *, actor: Any, cause: Any) -> InterruptStates: - """Define any actions to take on the actor if this task is interrupted. - - Note: - Custom Tasks can overwrite this method so they can handle being - interrupted with a custom procedure. By default, interrupt ends the - task. - - Args: - actor (Actor): the actor using the task - cause (Any): Optional data for the interrupt - """ - actor.log(f"Interrupted while performing {self}. Reasons: {cause}") - return self._interrupt_action - - def set_marker( - self, marker: str, interrupt_action: InterruptStates = InterruptStates.END - ) -> None: - """Set a marker to help with inspection of interrupts. - - The interrupt_action is set for when no `on_interrupt` is implemented. - - Args: - marker (str): String for the marker. - interrupt_action (InterruptStates, optional): Action to take on interrupt. - Defaults to InterruptStates.END. - """ - self._marker = marker - self._marked_time = self.env.now - self._interrupt_action = interrupt_action - - def get_marker(self) -> str | None: - """Get the current marker. - - Returns: - str | None: Marker (or None if cleared) - """ - return self._marker - - def get_marker_time(self) -> float | None: - """The time the current marker was set. - - Returns: - float | None: Marker set time (or None if cleared) - """ - return self._marked_time - - def clear_marker(self) -> None: - """Clear the marker and set that an interrupt ends the task.""" - self._marker = None - self._marked_time = None - self._interrupt_action = InterruptStates.END - - def _set_network_ref(self, network: "TaskNetwork") -> None: - """Set the reference to the task network object. - - Args: - network (TaskNetwork): The network - """ - if self._network_ref is not None: - raise SimulationError( - "Setting task network reference on task that already has a network" - ) - self._network_ref = network - - def _set_network_name(self, network_name: str) -> None: - """Set the name of the network this task is in. - - Args: - network_name (str): Network name - """ - if self._network_name is not None: - raise SimulationError("Setting task network name on task that already has a network") - self._network_name = network_name - - def clear_actor_task_queue(self, actor: "Actor") -> None: - """Clear out the task queue on the network. - - Args: - actor (Actor): The actor whose queue will be cleared - """ - assert self._network_name is not None - actor.clear_task_queue(self._network_name) - - def set_actor_task_queue(self, actor: "Actor", task_list: list[str]) -> None: - """Set the task queue on the actor. - - This assumes an empty queue. - - Args: - actor (Actor): The actor to modify the task queue of - task_list (list[str]): The list of task names to queue. - """ - assert self._network_name is not None - actor.set_task_queue(self._network_name, task_list) - - def get_actor_task_queue(self, actor: "Actor") -> list[str]: - """Get the task queue on the actor. - - Args: - actor (Actor): The actor to modify the task queue of - """ - assert self._network_name is not None - return actor.get_task_queue(self._network_name) - - def get_actor_next_task(self, actor: "Actor") -> str | None: - """Get the next queued task. - - Args: - actor (Actor): The actor to get the next task from - - Returns: - str | None: The next task name (or None if no task) - """ - assert self._network_name is not None - return actor.get_next_task(self._network_name) - - def set_actor_knowledge( - self, - actor: "Actor", - name: str, - value: Any, - overwrite: bool = False, - ) -> None: - """Set knowledge on the actor. - - Convenience method for passing in the name of task for actor logging. - - Args: - actor (Actor): The actor to set knowledge on. - name (str): Name of the knowledge - value (Any): Value of the knowledge - overwrite (bool, optional): Allow overwrite or not. Defaults to False. - """ - cname = self.__class__.__qualname__ - actor.set_knowledge(name, value, overwrite=overwrite, caller=cname) - - def clear_actor_knowledge(self, actor: "Actor", name: str) -> None: - """Clear knowledge from an actor. - - Convenience method for passing in the name of task for actor logging. - - Args: - actor (Actor): The actor to clear knowledge from - name (str): The name of the knowledge - """ - cname = self.__class__.__qualname__ - actor.clear_knowledge(name, caller=cname) - - @staticmethod - def get_actor_knowledge(actor: "Actor", name: str, must_exist: bool = False) -> Any: - """Get knowledge from the actor. - - Args: - actor (Actor): The actor to get knowledge from. - name (str): Name of the knowledge - must_exist (bool, optional): Raise errors if the knowledge doesn't exist. - Defaults to False. - - Returns: - Any: The knowledge value, which could be None - """ - return actor.get_knowledge(name, must_exist) - - def get_and_clear_actor_knowledge(self, actor: "Actor", name: str) -> Any: - """Get and clear knowledge on an actor. - - The knowledge is assumed to exist. - - Args: - actor (Actor): The actor to get knowledge from. - name (str): The knowledge name. - - Returns: - Any: The knowledge value. - """ - cname = self.__class__.__qualname__ - return actor.get_and_clear_knowledge(name, caller=cname) - - def set_actor_bulk_knowledge( - self, actor: "Actor", know: dict[str, Any], overwrite: bool = False - ) -> None: - """Set multiple knowledge entries at once. - - Args: - actor (Actor): The actor to operate on. - know (dict[str, Any]): Dictionary of key:value pairs of knowledge. - overwrite (bool, optional): If overwrite is allowed. Defaults to False. - """ - for k, v in know.items(): - self.set_actor_knowledge(actor, k, v, overwrite) - - def clear_actor_bulk_knowledge(self, actor: "Actor", names: Iterable[str]) -> None: - """Clear a list of knowledge entries. - - Args: - actor (Actor): The actor to operate on. - names (Iterable[str]): Knowledge names. - """ - for name in names: - self.clear_actor_knowledge(actor, name) - - def get_actor_bulk_knowledge( - self, actor: "Actor", names: Iterable[str], must_exist: bool = False - ) -> dict[str, Any]: - """Get multiple knowledge items. - - Args: - actor (Actor): The actor to operate on. - names (Iterable[str]): Names of the knowledge - must_exist (bool, optional): If all entires must exist. Defaults to False. - - Returns: - dict[str, Any]: The knowledge values. None if not present. - """ - return {name: self.get_actor_knowledge(actor, name, must_exist) for name in names} - - def get_and_clear_actor_bulk_knowledge( - self, actor: "Actor", names: Iterable[str], caller: str | None = None - ) -> dict[str, Any]: - """Get and clear multiple knowledge entries. - - Args: - actor (Actor): The actor to operate on. - names (Iterable[str]): The knowledge to retrieve and delete. - caller (str | None, optional): The name of the caller. Defaults to None. - - Returns: - dict[str, Any]: The retrieved knowledge. - """ - return {name: self.get_and_clear_actor_knowledge(actor, name) for name in names} - - def _clone_actor(self, actor: REH_ACTOR, knowledge: dict[str, Any] | None) -> REH_ACTOR: - """Create a clone of the actor. - - Args: - actor (Actor): The actor to clone - knowledge (dict[str, Any] | None): Additional knowledge to add. - - Returns: - Actor: Cloned actor - """ - mocked_env = MockEnvironment.mock(self.env) - self.env = mocked_env - understudy = actor.clone( - new_env=mocked_env, - knowledge=knowledge, - ) - return understudy - - def rehearse( - self, - *, - actor: REH_ACTOR, - knowledge: dict[str, Any] | None = None, - cloned_actor: bool = False, - **kwargs: Any, - ) -> REH_ACTOR: - """Rehearse the task to evaluate its feasibility. - - Args: - actor (Actor): The actor to rehearse in the task - knowledge (dict[str, Any], optional): Knowledge to add to the actor. Defaults to None. - cloned_actor (bool, optional): If the actor is a clone or not. Defaults to False. - kwargs (Any): Optional args to send to the task. - - Returns: - Actor: The cloned actor with a state reflecting the task flow. - """ - knowledge = {} if knowledge is None else knowledge - _old_env = self.env - understudy = actor - if not cloned_actor: - understudy = self._clone_actor(actor, knowledge) - if not isinstance(understudy.env, MockEnvironment): - raise SimulationError("Bad actor cloning.") - self.env = understudy.env - mocked_env: MockEnvironment = understudy.env - - self._rehearsing = True - generator = self.task(actor=understudy, **kwargs) - returned_item = None - while True: - try: - if returned_item is None: - next_event = next(generator) - else: - next_event = generator.send(returned_item) - returned_item = None - if not issubclass(next_event.__class__, BaseEvent | Routine): - msg = f"Task {self} event {next_event}" - if isinstance(next_event, Process): - raise SimulationError(msg + " cannot be a process during rehearsal.") - raise SimulationError(msg + " must be a subclass of BaseEvent or Routine.") - time_advance, returned_item = next_event.rehearse() - mocked_env.now += time_advance - - except StopIteration: - # warn(f"Stopping rehearsal of task '{self.__class__.__name__}' " - # f"for actor '{actor}'! [Rehearsal duration: " - # f"{self.env.now - _old_env.now:.3g}]") - break - - self.env = _old_env - self._rehearsing = False - return understudy - - def _handle_interruption( - self, actor: "Actor", interrupt: Interrupt, next_event: BaseEvent | Process - ) -> InterruptStates: - """Clean up after an interrupt and perform interrupt checks/actions. - - Args: - actor (Actor): _description_ - interrupt (Interrupt): _description_ - next_event (BaseEvent): _description_ - - Returns: - InterruptStates: action to take - """ - # test the interrupt behavior: - _interrupt_action = self.on_interrupt( - actor=actor, - cause=interrupt.cause, - ) - if _interrupt_action is None: - raise SimulationError("No interrupt behavior returned from `on_interrupt`") - - if _interrupt_action in (InterruptStates.END, InterruptStates.RESTART): - actor.log(f"Interrupted by {interrupt}.") - actor.deactivate_all_states(task=self) - actor.deactivate_all_mimic_states(task=self) - if isinstance(next_event, BaseEvent): - names = list(actor._knowledge.keys()) - for name in names: - if actor._knowledge[name] is next_event: - actor.clear_knowledge(name, caller=f"Clearing knowledge event {name}") - next_event.cancel() - elif isinstance(next_event, Process): - next_event.interrupt(cause=interrupt.cause) - else: - raise SimulationError(f"Bad event passed: {next_event}") - elif _interrupt_action != InterruptStates.IGNORE: - raise SimulationError(f"Wrong interrupt action value: {_interrupt_action}") - return _interrupt_action - - @process - def run(self, *, actor: "Actor") -> Generator[SimpyEvent | Process, Any, None]: - """Execute the task. - - Args: - actor (Actor): The actor using the task - - Returns: - Generator[SimpyEvent, Any, None] - """ - generators = [ - self.task(actor=actor), - ] - gen_objs: list[Task | Routine] = [ - self, - ] - return_item = None - stop_run = False - back_from_interrupt = False - event_to_yield: Process | SimpyEvent - while not stop_run: - try: - while True: - if not back_from_interrupt: - try: - if return_item is None: - next_event = next(generators[-1]) - else: - next_event = generators[-1].send(return_item) - except StopIteration as e: - generators.pop() - gen_objs.pop() - if e.value is not None: - return_item = e.value - if not generators: - stop_run = True - break - continue - - # A routine class gives us a generator, so go back to the - # start and run it as such. - if isinstance(next_event, Routine): - generators.append(next_event._run()) - gen_objs.append(next_event) - continue - - if isinstance(next_event, Process): - warn( - f"Yielding a simpy.Process from {self}. " - f"This is dangerous, take care. ", - UserWarning, - ) - event_to_yield = next_event - elif isinstance(next_event, BaseEvent): - event_to_yield = next_event.as_event() - else: - raise SimulationError(f"Unexpected yielded event type: {next_event}") - else: - back_from_interrupt = False - return_item = yield event_to_yield - # TODO: test if the return_item is for a multi-event - # that way we can return it as a more useful object - - except Interrupt as interrupt: - assert not isinstance(next_event, Routine) - action = self._handle_interruption( - actor, - interrupt, - next_event, - ) - if action == InterruptStates.IGNORE: - back_from_interrupt = True - else: - # This is restart or end; either way we have to cancel - # any routines in the stack. - while generators: - gen = generators.pop() - gen_obj = gen_objs.pop() - if isinstance(gen_obj, Routine): - gen.close() - yield from gen_obj._run_cancel() - if action == InterruptStates.RESTART: - generators = [ - self.task(actor=actor), - ] - gen_objs = [ - self, - ] - return_item = None - else: - stop_run = True - - -class DecisionTask(Task): - """A task used for decision processes.""" - - DO_NOT_HOLD = False - - def task(self, *, actor: Any) -> TASK_GEN: - """Define the process this task follows.""" - raise SimulationError("No need to call `task` on a DecisionTask") - - def rehearse_decision(self, *, actor: Any) -> None: - """Define the process this task follows.""" - raise NotImplementedError(NOT_IMPLEMENTED_MSG) - - def make_decision(self, *, actor: Any) -> None: - """Define the process this task follows.""" - raise NotImplementedError(NOT_IMPLEMENTED_MSG) - - def rehearse( - self, - *, - actor: REH_ACTOR, - knowledge: dict[str, Any] | None = None, - cloned_actor: bool = False, - **kwargs: Any, - ) -> REH_ACTOR: - """Rehearse the task to evaluate its feasibility. - - Args: - actor (Actor): The actor to rehearse with - knowledge (Optional[dict[str, Any]], optional): Knowledge to add. Defaults to None. - cloned_actor (bool, optional): If the actor is a clone or not. Defaults to False. - kwargs (Any): Kwargs for rehearsal. Kept for consistency to the base class. - - Returns: - Actor: Cloned actor after rehearsing this task. - """ - knowledge = {} if knowledge is None else knowledge - _old_env = self.env - understudy = actor - if not cloned_actor: - understudy = self._clone_actor(actor, knowledge) - self.env = understudy.env - - self._rehearsing = True - - self.rehearse_decision(actor=understudy) - self.env = _old_env - self._rehearsing = False - return understudy - - @process - def run(self, *, actor: "Actor") -> Generator[SimpyEvent, None, None]: - """Run the decision task. - - Args: - actor (Actor): The actor making decisions - - Yields: - Generator[SimpyEvent, None, None]: Generator for SimPy event queue. - """ - self.make_decision(actor=actor) - assert isinstance(self.env, SimpyEnv) - yield self.env.timeout(0.0) - - def run_skip(self, *, actor: "Actor") -> None: - """Run the decision task with no clock reference. - - Task networks will use this method if SKIP_WAIT is True. - - Args: - actor (Actor): The actor making decisions - """ - self.make_decision(actor=actor) - - -class TerminalTask(Task): - """A rehearsal-safe task that cannot exit, i.e., it is terminal. - - Note: - The user can re-implement the `log_message` method to return a custom - message that will be appended to the actor's log through its `log` - method. - """ - - _time_to_complete: float = 1e24 - - def log_message(self, *, actor: "Actor") -> str: - """A message to save to a log when this task is reached. - - Args: - actor (Actor): The actor using this task. - - Returns: - str: A log message - """ - return f"Entering terminal task: {self} on network {self._network_name}" - - def on_interrupt(self, *, actor: "Actor", cause: Any) -> InterruptStates: - """Special case interrupt for terminal task. - - Args: - actor (Actor): The actor - cause (Any): Additional data sent to the interrupt. - """ - raise SimulationError( - f"Cannot interrupt a terminal task {self} on {actor}. Kwargs sent: {cause}" - ) - return InterruptStates.END - - def task(self, *, actor: "Actor") -> TASK_GEN: - """The terminal task. - - It's just a long wait. - - Args: - actor (Actor): The actor - """ - log_message = self.log_message(actor=actor) - actor.log(log_message) - the_long_event = Event(rehearsal_time_to_complete=self._time_to_complete) - res = yield the_long_event - if res is not PLANNING_FACTOR_OBJECT: - raise SimulationError(f"A terminal task completed on {actor}") diff --git a/src/upstage_des/task_network.py b/src/upstage_des/task_network.py deleted file mode 100644 index 3c0c8c6..0000000 --- a/src/upstage_des/task_network.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""The task network class, and factory classes.""" - -from collections.abc import Generator, Mapping, Sequence -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, TypeVar - -if TYPE_CHECKING: - from upstage_des.actor import Actor - -from simpy import Process - -from upstage_des.base import SimulationError -from upstage_des.task import DecisionTask, Task, TerminalTask, process - -REH_ACTOR = TypeVar("REH_ACTOR", bound="Actor") - - -@dataclass -class TaskLinks: - """Type hinting for task link dictionaries.""" - - default: str | None - allowed: Sequence[str] - - -class TaskNetwork: - """A means to represent, execute, and rehearse interdependent tasks.""" - - def __init__( - self, - name: str, - task_classes: Mapping[str, type[Task]], - task_links: Mapping[str, TaskLinks], - ) -> None: - """Create a task network. - - Task links are defined as: - {task_name: TaskLinks(default= task_name | None, allowed= list[task_names]} - where each task has a default next task (or None), and tasks that could follow it. - - Args: - name (str): Network name - task_classes (Mapping[str, Task]): Task names to Task object mapping. - task_links (Mapping[str, TaskLinks]): Task links. - """ - self.name = name - self.task_classes = task_classes - self.task_links = task_links - self._current_task_name: str | None = None - self._current_task_inst: Task | None = None - self._current_task_proc: Process | None = None - - def is_feasible(self, curr: str, new: str) -> bool: - """Determine if a task can follow another one. - - Args: - curr (str): Current task name - new (str): Potential next task name - - Returns: - bool: If the new task can follow the current. - """ - value = self.task_links[curr].allowed - return new in value - - def _next_task_name( - self, curr_task_name: str, actor: "Actor", clear_queue: bool = False - ) -> str: - """Get the next task name. - - Returns: - str: Task name - """ - task_from_queue = actor.get_next_task(self.name) - default_next_task = self.task_links[curr_task_name].default - if task_from_queue is None: - if default_next_task is None: - raise SimulationError( # pramga: no cover - f"No default task set for after {curr_task_name} on {actor}." - ) - next_name = default_next_task - else: - next_name = task_from_queue - # once we have the name, pop it from the queue - if clear_queue: - actor._clear_task(self.name) - return next_name - - @process - def loop( - self, *, actor: "Actor", init_task_name: str | None = None - ) -> Generator[Process, None, None]: - """Start a task network running its loop. - - If no initial task name is given, it will default to following the queue. - - Args: - actor (Actor): The actor to run the loop on. - init_task_name (Optional[str], optional): Optional task to start running. - Defaults to None. - """ - next_name = actor.get_next_task(self.name) - if next_name is None: - if init_task_name is None: - raise SimulationError( - f"Actor {actor} wasn't supplied an initial task" - ) # pramga: no cover - next_name = init_task_name - - self._current_task_name = next_name - - while True: - task_name = self._current_task_name - assert isinstance(task_name, str) - actor.log(f"Outer: starting {task_name}") - actor._begin_next_task(self.name, task_name) - task_cls = self.task_classes[task_name] - task_instance: Task = task_cls() - self._current_task_inst = task_instance - self._current_task_inst._set_network_name(self.name) - self._current_task_inst._set_network_ref(self) - - if ( - isinstance(self._current_task_inst, DecisionTask) - and self._current_task_inst.DO_NOT_HOLD - ): - self._current_task_inst.run_skip(actor=actor) - else: - self._current_task_proc = self._current_task_inst.run(actor=actor) - yield self._current_task_proc - - next_name = self._next_task_name(task_name, actor) - self._current_task_name = next_name - - def rehearse_network( - self, - *, - actor: REH_ACTOR, - task_name_list: list[str], - knowledge: dict[str, Any] | None = None, - end_task: str | None = None, - ) -> REH_ACTOR: - """Rehearse a path through the task network. - - Args: - actor (Actor): The actor to perform the task rehearsal withs - task_name_list (list[str]): The tasks to be performed in order - knowledge (dict[str, Any], optional): Knowledge to give to the cloned/rehearsing actor - end_task (str, optional): A task name to end on - - Returns: - Actor: A copy of the original actor with state changes associated with the network. - """ - _old_name = self._current_task_name - _old_inst = self._current_task_inst - _old_proc = self._current_task_proc - knowledge = {} if knowledge is None else knowledge - num_tasks = len(task_name_list) - # pre-clone the actor to get a hold of the new environment - new_actor = actor.clone(knowledge=knowledge) - task_idx = 0 - while True: - if task_idx < num_tasks: - task_name = task_name_list[task_idx] - elif end_task is None: - break - else: - # Grab the default or one from the queue, clearing the queue to prevent loops - task_name = self._next_task_name(task_name, new_actor, clear_queue=True) - if end_task is not None and end_task == task_name: - break # pragma: no cover - self._current_task_name = task_name - self._current_task_inst = self.task_classes[task_name]() - self._current_task_inst._set_network_name(self.name) - new_actor = self._current_task_inst.rehearse( - actor=new_actor, - cloned_actor=True, - ) - # The next name should be feasible - if task_idx < num_tasks - 1: - follow_on = task_name_list[task_idx + 1] - if not self.is_feasible(task_name, follow_on): - raise SimulationError( # pragma: no cover - f"Task {follow_on} not allowed after '{task_name}' in network" - ) - task_idx += 1 - # reset the internal parameters - self._current_task_name = _old_name - self._current_task_inst = _old_inst - self._current_task_proc = _old_proc - return new_actor - - def __repr__(self) -> str: - return f"Task network: {self.name}" - - -class TaskNetworkFactory: - """A factory for creating task network instances.""" - - def __init__( - self, - name: str, - task_classes: Mapping[str, type[Task]], - task_links: Mapping[str, TaskLinks], - ) -> None: - """Create a factory for making instances of a task network. - - Task links are defined as: - {task_name: TaskLinks(default= task_name | None, allowed= list[task_names]} - where each task has a default next task (or None), and tasks that could follow it. - - Args: - name (str): The network name - task_classes (dict[str, Task]): Network task classes - task_links (dict[str, dict[str, str | list[str] | None]]): Network links. - """ - self.name = name - self.task_classes = task_classes - self.task_links = task_links - - @classmethod - def from_single_looping(cls, name: str, task_class: type[Task]) -> "TaskNetworkFactory": - """Create a network factory from a single task that loops. - - Args: - name (str): Network name - task_class (Task): The single task to loop - - Returns: - TaskNetworkFactory: The factory for the single looping network. - """ - taskname = task_class.__name__ - task_classes = {taskname: task_class} - task_links: dict[str, TaskLinks] = { - taskname: TaskLinks(default=taskname, allowed=[taskname]) - } - return TaskNetworkFactory(name, task_classes, task_links) - - @classmethod - def from_single_terminating(cls, name: str, task_class: type[Task]) -> "TaskNetworkFactory": - """Create a network factory from a single task that terminates. - - Args: - name (str): Network name - task_class (Task): The single task to terminate after - - Returns: - TaskNetworkFactory: The factory for the single terminating network. - """ - taskname = task_class.__name__ - end_name = f"{taskname}_FINAL" - task_classes = {taskname: task_class, end_name: TerminalTask} - task_links: dict[str, TaskLinks] = { - taskname: TaskLinks(default=end_name, allowed=[end_name]) - } - return TaskNetworkFactory(name, task_classes, task_links) - - @classmethod - def from_ordered_terminating( - cls, name: str, task_classes: list[type[Task]] - ) -> "TaskNetworkFactory": - """Create a network factory from a list of tasks that terminates. - - Args: - name (str): Network name - task_classes (list[Task]): The tasks to run in order. - - Returns: - TaskNetworkFactory: The factory for the ordered network. - """ - task_class = {} - task_links: dict[str, TaskLinks] = {} - for i, tc in enumerate(task_classes): - the_name = tc.__name__ - task_class[the_name] = tc - try: - nxt = task_classes[i + 1] - nxt_name = nxt.__name__ - except IndexError: - nxt = TerminalTask - nxt_name = f"{name}_TERMINATING" - task_class[nxt_name] = nxt - task_links[the_name] = TaskLinks(default=nxt_name, allowed=[nxt_name]) - return TaskNetworkFactory(name, task_class, task_links) - - @classmethod - def from_ordered_loop(cls, name: str, task_classes: list[type[Task]]) -> "TaskNetworkFactory": - """Create a network factory from a list of tasks that loops. - - Args: - name (str): Network name - task_classes (list[Task]): The tasks to run in order. - - Returns: - TaskNetworkFactory: The factory for the ordered network. - """ - task_class = {} - task_links: dict[str, TaskLinks] = {} - for i, tc in enumerate(task_classes): - the_name = tc.__name__ - task_class[the_name] = tc - try: - nxt = task_classes[i + 1] - except IndexError: - nxt = task_classes[0] - nxt_name = nxt.__name__ - task_links[the_name] = TaskLinks(default=nxt_name, allowed=[nxt_name]) - return TaskNetworkFactory(name, task_class, task_links) - - def make_network(self, other_name: str | None = None) -> TaskNetwork: - """Create an instance of the task network. - - By default, this uses the name defined on instantiation. - - Args: - other_name (str, optional): Another name for the network. Defaults to None. - - Returns: - TaskNetwork - """ - use_name = other_name if other_name is not None else self.name - return TaskNetwork(use_name, self.task_classes, self.task_links) diff --git a/src/upstage_des/test/__init__.py b/src/upstage_des/test/__init__.py deleted file mode 100644 index eb3d8f3..0000000 --- a/src/upstage_des/test/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Module for testing UPSTAGE.""" diff --git a/src/upstage_des/test/conftest.py b/src/upstage_des/test/conftest.py deleted file mode 100644 index 7b47faf..0000000 --- a/src/upstage_des/test/conftest.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Fixtures for testing.""" - -from typing import Any - -import pytest - -import upstage_des.api as UP - - -@pytest.fixture -def base_actors() -> tuple[tuple[UP.State, ...], tuple[type[UP.Actor], ...]]: - """State and Actor classes for testing. - - Returns: - tuple[tuple[UP.State, ...], tuple[UP.Actor, ...]]: States and Actors. - """ - first_state = UP.State[Any]() - second_state = UP.State[Any]() - third_state = UP.State[Any]() - fourth_state = UP.State[Any]() - - class ActorSubclass(UP.Actor): - state_one = first_state - state_two = second_state - - def a_function(self, inp: Any) -> tuple[UP.Actor, Any]: - return self, inp - - class DoubleSubclass(ActorSubclass): - state_three = third_state - state_four = fourth_state - - def b_function(self, inp: Any) -> tuple[UP.Actor, Any]: - return self, inp - - states = (first_state, second_state, third_state, fourth_state) - actors = (ActorSubclass, DoubleSubclass) - - return states, actors - - -@pytest.fixture -def task_objects() -> tuple[type[UP.TerminalTask], type[UP.TerminalTask], type[UP.Actor]]: - """Example task objects for testing. - - Returns: - tuple[UP.Task, UP.Task, UP.Actor]: The task objects. - """ - - class EndPoint(UP.TerminalTask): - def log_message(self, *, actor: UP.Actor) -> str: - return "The Message" - - class EndPointBase(UP.TerminalTask): - pass - - class Dummy(UP.Actor): - status = UP.State[Any]() - - return EndPoint, EndPointBase, Dummy diff --git a/src/upstage_des/test/test_actor.py b/src/upstage_des/test/test_actor.py deleted file mode 100644 index 2c95211..0000000 --- a/src/upstage_des/test/test_actor.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from inspect import signature -from typing import Any - -import pytest - -import upstage_des.api as UP -from upstage_des.actor import Actor -from upstage_des.base import EnvironmentContext, SimulationError -from upstage_des.states import State - - -def test_actor_creation() -> None: - """Ensure instantation of Actor.""" - with EnvironmentContext(): - name = "testing" - actor = Actor(name=name) - assert actor.__class__ == Actor - assert hasattr(actor, "_state_defs") - - with EnvironmentContext(): - a = Actor(name="Three") - actors = a.get_actors() - assert len(actors) == 1 - assert a in actors - - -def test_actor_subclass( - base_actors: tuple[tuple[UP.State, ...], tuple[type[UP.Actor], ...]], -) -> None: - """Test subclasses of actor. - - A subclass of actor must have a proper signature and states in its - dictionary. - - The names of the states also need to match up. - - Any functions must also be a part of the subclass. - - """ - states, actors = base_actors - first_state, second_state, third_state, fourth_state = states - ActorSubclass, DoubleSubclass = actors - - # is the new actor a subclass? - assert issubclass(ActorSubclass, Actor) - - # does the init signature include the values we want? - actor_signature = signature(ActorSubclass.__init__) - expected_params = ["state_one", "state_two", "name"] - for parameter in expected_params: - assert parameter in actor_signature.parameters - assert "kwargs" not in actor_signature.parameters - - # test making an instance without arguments raising an error - with pytest.raises(Exception): - ActorSubclass() # type: ignore [call-arg] - - with EnvironmentContext(): - # create an instance - instance = ActorSubclass( - name="Testing", - state_one=1, - state_two=2, - ) - - assert instance.a_function(123) == (instance, 123) # type: ignore [attr-defined] - - # test the state definitions - assert hasattr(instance, "_state_defs") - assert len(instance._state_defs) == 2 - assert instance._state_defs["state_one"] is first_state - assert instance._state_defs["state_two"] is second_state - assert repr(instance) == "ActorSubclass: Testing" - sts = instance.state_values - exp = {"state_one": 1, "state_two": 2} - assert sts == exp - - # Test that copying a state name will cause a failure. - with pytest.raises(ValueError, match="Duplicated state name"): - - class _(DoubleSubclass): # type: ignore [valid-type, misc] - state_three = UP.State[float](default=1.2) - - -def test_multiple_inheritance( - base_actors: tuple[tuple[UP.State, ...], tuple[type[UP.Actor], ...]], -) -> None: - """Test actor subclasses but for an additional subclass.""" - states, actors = base_actors - first_state, second_state, third_state, fourth_state = states - ActorSubclass, DoubleSubclass = actors - - assert issubclass(DoubleSubclass, Actor) - assert issubclass(DoubleSubclass, ActorSubclass) - - actor_signature = signature(DoubleSubclass.__init__) - expected_params = ["state_one", "state_two", "state_three", "name"] - for parameter in expected_params: - assert parameter in actor_signature.parameters - assert "kwargs" not in actor_signature.parameters - - with EnvironmentContext(): - instance = DoubleSubclass( - name="Testing", - state_one=1, - state_two=2, - state_three=3, - state_four=4, - ) - assert instance.b_function(123) == (instance, 123) # type: ignore [attr-defined] - - # test the state definitions - assert hasattr(instance, "_state_defs") - assert len(instance._state_defs) == 4 - assert "state_three" in instance.states - assert "state_one" in instance.states - assert instance._state_defs["state_one"] is first_state - assert instance._state_defs["state_three"] is third_state - assert instance._state_defs["state_four"] is fourth_state - - -def test_get_knowledge() -> None: - class TestActor(Actor): - pass - - with EnvironmentContext(): - act = TestActor(name="A Test Actor", initial_knowledge={"initone": set([1, 2, 3])}) - - name = "new" - other_name = "some data" - value = {"A": 1, "B": 2} - act.set_knowledge(name, value) - - returned_value = act.get_knowledge(name) - assert value == returned_value, "Returned value is not the same knowledge" - - other_value = act.get_knowledge(other_name) - assert other_value is None, "Empty knowledge returned something other than None" - - init_know = act.get_knowledge("initone") - assert init_know == set([1, 2, 3]) - - -def test_set_knowledge() -> None: - class TestActor(Actor): - pass - - with EnvironmentContext(): - act = TestActor(name="A Test Actor") - - name = "new" - value = {"A": 1, "B": 2} - value2 = {"A": 5, "B": 6} - act.set_knowledge(name, value) - - with pytest.raises(SimulationError): - act.set_knowledge(name, value2) - - act.set_knowledge(name, value2, overwrite=True) - returned_value = act.get_knowledge(name) - assert value2 == returned_value, "Returned value is not the same knowldge" - - -def test_clear_knowledge() -> None: - class TestActor(Actor): - pass - - with EnvironmentContext(): - act = TestActor(name="A Test Actor") - - name = "new" - value = {"A": 1, "B": 2} - act.set_knowledge(name, value) - - act.get_knowledge(name) - - act.clear_knowledge(name) - know = act.get_knowledge(name) - assert know is None, "Knowledge was not cleared" - - -def test_get_and_clear() -> None: - class TestActor(Actor): - pass - - with EnvironmentContext(): - act = TestActor(name="Second test") - act.set_knowledge("thing", {3: 1}) - v = act.get_and_clear_knowledge("thing") - assert v == {3: 1} - assert "thing" not in act._knowledge - - with pytest.raises(UP.SimulationError): - act.get_and_clear_knowledge("thing") - - t = UP.Task() - act.set_knowledge("other", {2: 3}) - v = t.get_and_clear_actor_knowledge(act, "other") - assert v == {2: 3} - assert "other" not in act._knowledge - - -def test_bulk_knowledge() -> None: - class TestActor(Actor): - pass - - with UP.EnvironmentContext(): - know = {"one": 1, "two": 2} - act = TestActor(name="Example") - act.set_bulk_knowledge(know) - assert know == act._knowledge - - with pytest.raises(UP.SimulationError): - act.set_bulk_knowledge({"one": 3, "three": 3}) - - act.set_bulk_knowledge({"one": 11, "three": 3}, overwrite=True) - v = act.get_bulk_knowledge(set(["one", "two", "three"])) - assert v == {"one": 11, "two": 2, "three": 3} - - v = act.get_and_clear_bulk_knowledge(["two", "three"]) - assert v == {"two": 2, "three": 3} - assert act._knowledge == {"one": 11} - - t = UP.Task() - t.set_actor_bulk_knowledge(act, know, overwrite=True) - assert act._knowledge == know - v = t.get_actor_bulk_knowledge(act, ["one", "two"]) - assert v == know - - with pytest.raises(UP.SimulationError): - t.set_actor_bulk_knowledge(act, {"one": 3, "three": 3}) - - v = t.get_and_clear_actor_bulk_knowledge(act, ["one", "two"]) - assert v == know - assert act._knowledge == {} - - -def test_knowledge_event() -> None: - with EnvironmentContext() as env: - act = Actor(name="A test actor") - evt = act.create_knowledge_event(name="Waiter") - assert act._knowledge.get("Waiter", None) is evt - assert evt.is_complete() is False - act.succeed_knowledge_event(name="Waiter") - env.run() - assert act._knowledge.get("Waiter", None) is None - assert evt.is_complete() - - -def test_actor_copying() -> None: - class SomeActor(Actor): - kind = "a simple actor for testing" - some_state = State[Any]() - - with EnvironmentContext(): - actor = SomeActor(name="some actor", some_state=True) - - clone = actor.clone(new_env=None, some_state=False) - - assert clone.some_state != actor.some_state - - assert actor.name in clone.name - - assert "[CLONE" in clone.name - - assert actor._num_clones == 1 - - assert actor.kind == clone.kind - - -def test_actor_copy_with_knowledge() -> None: - class SomeActor(Actor): - kind = "a simple actor for testing" - some_state = State[Any]() - - with EnvironmentContext(): - actor = SomeActor(name="some actor", some_state=True) - d_values = {"A": 1, "B": 2} - float_value = 1234.567 - - actor.set_knowledge("new", d_values) - actor.set_knowledge("other", float_value) - - clone = actor.clone(new_env=None, some_state=False) - for name in ["new", "other"]: - v1 = actor.get_knowledge(name) - v2 = clone.get_knowledge(name) - assert v1 == v2, "Copied knowledge is different" - - # we can't test for equal IDs or object equivalence because of how - # Python handles memory for booleans, small integers, etc. - - d_values["A"] = 23 - v1 = actor.get_knowledge("new") - assert v1["A"] == 23, "Input knowledge did not retain reference" - - v2 = clone.get_knowledge("new") - assert v2["A"] == 1, "Cloned knowledge retained reference" - - -def test_no_init_state() -> None: - with pytest.raises(SimulationError, match="needs a default for no_init=True"): - - class BadActor(UP.Actor): - st = UP.State[int](no_init=True) - - class NoInitExample(UP.Actor): - a = UP.State[int](default=0, no_init=True) - b = UP.State[float](default_factory=lambda: 3.0, no_init=True) - c = UP.State[str]() - - with UP.EnvironmentContext(): - act = NoInitExample( - name="exam", - c="hello", - ) - assert act.a == 0 - assert act.b == 3.0 - assert act.c == "hello" - - with pytest.raises(SimulationError, match="Initializing a no_init state is disallowed"): - NoInitExample(name="exam", a=2, c="hello") diff --git a/src/upstage_des/test/test_api.py b/src/upstage_des/test/test_api.py deleted file mode 100644 index b3b7c6a..0000000 --- a/src/upstage_des/test/test_api.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from upstage_des import api - - -def test_api() -> None: - api_items = dir(api) - - items_to_test = ( - "UpstageError", - "SimulationError", - "RulesError", - "Actor", - "PLANNING_FACTOR_OBJECT", - "UpstageBase", - "NamedUpstageEntity", - "EnvironmentContext", - "add_stage_variable", - "get_stage_variable", - "get_stage", - "All", - "Any", - "Event", - "Get", - "FilterGet", - "SortedFilterGet", - "Put", - "ResourceHold", - "Wait", - "ContainerEmptyError", - "ContainerError", - "ContainerFullError", - "ContinuousContainer", - "SelfMonitoringContainer", - "SelfMonitoringContinuousContainer", - "SelfMonitoringFilterStore", - "SelfMonitoringSortedFilterStore", - "SelfMonitoringReserveContainer", - "SelfMonitoringStore", - "ReserveContainer", - "SortedFilterStore", - "Location", - "CartesianLocation", - "GeodeticLocation", - "CartesianLocationData", - "GeodeticLocationData", - "LinearChangingState", - "DataclassState", - "DictionaryState", - "CartesianLocationChangingState", - "State", - "GeodeticLocationChangingState", - "DetectabilityState", - "ResourceState", - "MultiStoreState", - "DecisionTask", - "Task", - "process", - "InterruptStates", - "TerminalTask", - "TaskNetwork", - "TaskNetworkFactory", - "TaskLinks", - "PointToPointCommsManager", - "RoutingTableCommsManager", - "Message", - "MessageContent", - "MotionAndDetectionError", - "SensorMotionManager", - "SteppedMotionManager", - "TaskNetworkNucleus", - "NucleusInterrupt", - "SharedLinearChangingState", - "CommunicationStore", - "unit_convert", - "Routine", - "WindowedGet", - ) - - for item in items_to_test: - assert item in api_items - - for item in api_items: - if not item.startswith("_"): - assert item in items_to_test diff --git a/src/upstage_des/test/test_base.py b/src/upstage_des/test/test_base.py deleted file mode 100644 index ca6deb1..0000000 --- a/src/upstage_des/test/test_base.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) -# Licensed under the 3-Clause BSD License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -import multiprocessing as mp - -import pytest -import simpy as SIM - -from upstage_des.base import ( - STAGE_CONTEXT_VAR, - EnvironmentContext, - NamedUpstageEntity, - UpstageBase, - UpstageError, - add_stage_variable, -) - - -def test_context() -> None: - with EnvironmentContext() as env: - assert isinstance(env, SIM.Environment) - env.run(until=3) - assert env.now == 3 - - with EnvironmentContext(initial_time=3.1) as env: - assert isinstance(env, SIM.Environment) - assert env.now == 3.1 - - -def test_entity_naming() -> None: - class TestItem(NamedUpstageEntity): ... - - class Example(TestItem, entity_groups=["This"]): ... - - class Ignorable(NamedUpstageEntity, add_to_entity_groups=False): ... - - # This shows how the entity group ignorance doesn't cascade down. - class Ignorable2(Ignorable): ... - - with pytest.warns(UserWarning, match="Environment not created*"): - other = Example() - - with pytest.raises(UpstageError): - other.env - - with EnvironmentContext() as env: - # Note that due to the context, we can access the environment - # even if we couldn't before. - assert env is other.env - - t = TestItem() - t2 = TestItem() - e = Example() - _ = Ignorable() - _ = Ignorable2() - - items = t.get_entity_group("TestItem") - assert len(items) == 3 - assert t in items - assert t2 in items - assert e in items - - items = t.get_entity_group("This") - assert len(items) == 1 - assert e in items - - items = t.get_entity_group("Example") - assert len(items) == 1 - assert e in items - - items = t.get_entity_group("Ignorable") - assert len(items) == 0 - - items = t.get_entity_group("Ignorable2") - assert len(items) == 1 - - -def test_stage() -> None: - with EnvironmentContext(): - add_stage_variable("A variable", 3.14) - ans = STAGE_CONTEXT_VAR.get() - assert ans.get("A variable", 0.1) == 3.14 - assert ans.get("random") is not None - assert len(ans) == 2 - with pytest.raises(UpstageError): - add_stage_variable("A variable", 2) - - -def test_random() -> None: - with EnvironmentContext(random_seed=1234986): - cl = UpstageBase() - num = cl.stage.random.uniform(1, 3) - assert pytest.approx(num) == 2.348057489610457 - - from random import Random - - rng = Random(1234986) - with EnvironmentContext(random_gen=rng): - cl = UpstageBase() - num = cl.stage.random.uniform(1, 3) - assert pytest.approx(num) == 2.348057489610457 - - with EnvironmentContext(): - num = cl.stage.random.uniform(1, 3) - - -def a_simulation(t: float) -> float: - with EnvironmentContext() as env: - env.run(until=env.now + t) - return env.now - - -def test_multiproc_stability() -> None: - inputs = [1.2, 3.4, 5.6, 10.11, 74.31] - - with mp.Pool(3) as pool: - res = pool.map(a_simulation, inputs) - - assert res == inputs - - -@pytest.mark.parametrize( - ["unit", "mult"], - [ - ("min", 60), - ("Minutes", 60), - ("s", 3600), - ("second", 3600), - ("hours", 1), - ("hr", 1), - (None, 1), - ], -) -def test_pretty_times(unit: str, mult: int) -> None: - """Test that if we do time in regular ways, we get standard logging.""" - times_in_hours = [3, 28, 24 * 15 + 6.5] - times_in_hours = [x * mult for x in times_in_hours] - with EnvironmentContext(initial_time=times_in_hours[0]) as env: - add_stage_variable("time_unit", unit) - base = UpstageBase() - assert base.pretty_now == "[Day 0 - 03:00:00]" - env.run(until=times_in_hours[1]) - assert base.pretty_now == "[Day 1 - 04:00:00]" - env.run(until=times_in_hours[2]) - assert base.pretty_now == "[Day 15 - 06:30:00]" - - -@pytest.mark.parametrize("unit", ["ticks", "week", "day", "microseconds"]) -def test_pretty_time_nonstandard(unit: str) -> None: - with EnvironmentContext() as env: - add_stage_variable("time_unit", unit) - base = UpstageBase() - assert base.pretty_now == f"[0.000 {unit}]" - add_stage_variable("daily_time_count", 100) - assert base.pretty_now == f"[Day 0 - 0.000 {unit}]" - env.run(until=223) - assert base.pretty_now == f"[Day 2 - 23.000 {unit}]" diff --git a/src/upstage_des/test/test_comms.py b/src/upstage_des/test/test_comms.py deleted file mode 100644 index 2a4cc02..0000000 --- a/src/upstage_des/test/test_comms.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Any - -import pytest -from simpy import Store - -import upstage_des.api as UP -from upstage_des.api import ( - Actor, - EnvironmentContext, - Get, - Message, - MessageContent, - PointToPointCommsManager, - ResourceState, - State, - Task, - Wait, -) -from upstage_des.communications.processes import generate_comms_wait -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class ReceiveSend(Actor): - incoming = ResourceState[Store](default=Store) - result = State[Any](default="None") - - -class ReceiveTask(Task): - def task(self, *, actor: ReceiveSend) -> TASK_GEN: - item = yield Get(actor.incoming) - actor.result = item - - -class SendTask(Task): - comms: PointToPointCommsManager - receiver: ReceiveSend - - def task(self, *, actor: ReceiveSend) -> TASK_GEN: - yield Wait(1.0) - content = MessageContent(data=dict(action="move", thought="good")) - message = Message(actor, content, self.receiver) - yield self.comms.make_put(message, actor, self.receiver) - - -def test_send_receive() -> None: - with EnvironmentContext() as env: - receiver = ReceiveSend(name="recv") - sender = ReceiveSend(name="send") - - rec_task = ReceiveTask() - sen_task = SendTask() - - comms = PointToPointCommsManager( - name="Comm", - init_entities=[(receiver, "incoming")], - debug_logging=True, - ) - comms.run() - - rec_task.run(actor=receiver) - sen_task.comms = comms - sen_task.receiver = receiver - sen_task.run(actor=sender) - - env.run() - - assert env.now == 1.0, "Wrong simulation end time for comms" - assert receiver.result != "None", "No result for comms" - assert isinstance(receiver.result, Message), "Wrong result format" - content = receiver.result.content.data - assert content["action"] == "move" - assert content["thought"] == "good" - - -def test_send_receive_delayed() -> None: - with EnvironmentContext() as env: - receiver = ReceiveSend(name="recv") - sender = ReceiveSend(name="send") - - rec_task = ReceiveTask() - sen_task = SendTask() - - comms = PointToPointCommsManager( - name="Comm", - send_time=0.25, - debug_logging=True, - ) - comms.connect(receiver, "incoming") - comms.run() - - rec_task.run(actor=receiver) - sen_task.comms = comms - sen_task.receiver = receiver - sen_task.run(actor=sender) - - env.run() - - assert env.now == 1.25, "Wrong simulation end time for comms" - assert receiver.result != "None", "No result for comms" - assert isinstance(receiver.result, Message), "Wrong result format" - content = receiver.result.content.data - assert content["action"] == "move" - assert content["thought"] == "good" - - -def test_degraded() -> None: - with EnvironmentContext() as env: - receiver = ReceiveSend(name="recv") - sender = ReceiveSend(name="send") - - rec_task = ReceiveTask() - sen_task = SendTask() - - comms = PointToPointCommsManager( - name="Comm", - send_time=0.25, - debug_logging=True, - ) - comms.comms_degraded = True - comms.connect(receiver, "incoming") - comms.run() - - rec_task.run(actor=receiver) - sen_task.comms = comms - sen_task.receiver = receiver - sen_task.run(actor=sender) - - env.run(until=4) - comms.comms_degraded = False - - assert receiver.result == "None" - - -def test_blocked() -> None: - with EnvironmentContext() as env: - receiver = ReceiveSend(name="recv") - sender = ReceiveSend(name="send") - - rec_task = ReceiveTask() - sen_task = SendTask() - - comms = PointToPointCommsManager( - name="Comm", - send_time=0.25, - debug_logging=True, - ) - comms.connect(receiver, "incoming") - comms.run() - - comms.blocked_links.append((sender, receiver)) - - rec_task.run(actor=receiver) - sen_task.comms = comms - sen_task.receiver = receiver - sen_task.run(actor=sender) - - env.run(until=4) - comms.comms_degraded = False - - assert receiver.result == "None" - - -def test_comms_wait() -> None: - with UP.EnvironmentContext() as env: - store = Store(env=env) - data_point = [] - - def cback(message: MessageContent) -> None: - data_point.append(message) - - msg = Message( - sender=UP.Actor(name="me"), - content=MessageContent(data={"hello": "world"}), - destination=UP.Actor(name="you"), - ) - wait_proc = generate_comms_wait(store, cback) - wait_proc() - - store.put(msg) - env.run(until=1) - assert len(data_point) == 1 - - -class Worker(UP.Actor): - walkie = UP.CommunicationStore(modes=["UHF", "other"]) - intercom = UP.CommunicationStore(modes="loudspeaker") - - -def test_worker_talking() -> None: - with EnvironmentContext() as env: - w1 = Worker(name="worker1") - w2 = Worker(name="worker2") - - uhf_comms = PointToPointCommsManager(name="Walkies", mode="UHF") - loudspeaker_comms = PointToPointCommsManager(name="Overhead", mode="loudspeaker") - - uhf_comms.run() - loudspeaker_comms.run() - - evt1 = uhf_comms.make_put("Hello worker", w1, w2) - evt2 = loudspeaker_comms.make_put("Hello worker", w2, w1) - - def do() -> SIMPY_GEN: - yield evt1.as_event() - yield evt2.as_event() - - env.process(do()) - - env.run() - assert len(w2.walkie.items) == 1 - assert w2.walkie.items[0].mode == "UHF" - assert len(w1.intercom.items) == 1 - assert w1.intercom.items[0].mode == "loudspeaker" - - -class CommNode(Actor): - messages = UP.CommunicationStore(modes=None) - - -def _build_net(two_way: bool = False) -> tuple[dict[str, CommNode], UP.RoutingTableCommsManager]: - nodes = { - name: CommNode(name=name, messages={"modes": ["cup-and-string"]}) for name in "ABCDEFGH" - } - mgr = UP.RoutingTableCommsManager( - name="StaticManager", - mode="cup-and-string", - send_time=1 / 3600.0, - retry_max_time=20 / 3600.0, - retry_rate=4 / 3600.0, - debug_logging=True, - ) - # Set up the routes - opts = [ - ("A", "B"), - ("B", "C"), - ("A", "D"), - ("D", "E"), - ("E", "F"), - ("F", "G"), - ("G", "H"), - ("H", "C"), - ("E", "B"), - ] - for u, v in opts: - mgr.connect_nodes(nodes[u], nodes[v], two_way=two_way) - return nodes, mgr - - -def _start( - msg: str, mgr: UP.RoutingTableCommsManager, nodes: dict[str, CommNode], source: str, dest: str -) -> SIMPY_GEN: - put = mgr.make_put(msg, nodes[source], nodes[dest]) - yield put.as_event() - - -def test_routing_basic() -> None: - with EnvironmentContext() as env: - nodes, mgr = _build_net() - # Check the shortest path calcs with node dropouts. - nxt = mgr.select_hop(nodes["A"], nodes["C"]) - assert nxt is nodes["B"] - nxt = mgr.select_hop(nodes["A"], nodes["C"], [nodes["B"]]) - assert nxt is nodes["D"] - nxt = mgr.select_hop(nodes["A"], nodes["C"], [nodes["B"], nodes["D"]]) - assert not nxt - - # Asking for a node not in the network will fail - notin = CommNode(name="not in", messages={"modes": ["doesn'thave"]}) - nxt = mgr.select_hop(notin, nodes["C"], [nodes["B"], nodes["D"]]) - assert nxt is None - - with pytest.raises(UP.SimulationError, match="has no comms store on mode"): - mgr.connect_nodes(notin, nodes["C"]) - - # Test the actual routing mechanics - # Run the manager - mgr.run() - env.process(_start("First message", mgr, nodes, "A", "C")) - env.run() - # Takes two hops to reach the destination - assert env.now == 1 / 3600 * 2.0 - assert len(mgr.debug_log) == 3 - assert mgr.debug_log[0]["time"] == 0 - assert mgr.debug_log[0]["event"] == "Moved message" - assert mgr.debug_log[0]["current"] == nodes["A"] - assert mgr.debug_log[0]["destination"] == nodes["B"] - assert mgr.debug_log[1]["time"] == 1 / 3600 - assert mgr.debug_log[1]["event"] == "Moved message" - assert mgr.debug_log[1]["current"] == nodes["B"] - assert mgr.debug_log[1]["destination"] == nodes["C"] - assert mgr.debug_log[2]["time"] == 1 / 3600 * 2 - assert mgr.debug_log[2]["event"] == "Destination reached" - assert len(nodes["C"].messages.items) == 1 - assert nodes["C"].messages.items[0].content.message == "First message" - - nodes, mgr = _build_net(two_way=True) - mgr.run() - env.process(_start("First message", mgr, nodes, "C", "A")) - env.run() - assert len(mgr.debug_log) == 3 - assert nodes["A"].messages.items[0].content.message == "First message" - assert nodes["B"].messages.items == [] - - -def test_routing_drop_node() -> None: - """When we drop a node, expect it to take longer. - - This doesn't have global on, which means B will be tried twice. - """ - with EnvironmentContext() as env: - nodes, mgr = _build_net() - mgr.run() - mgr.blocked_nodes.append(nodes["B"]) - env.process(_start("Second message", mgr, nodes, "A", "C")) - env.run() - expected_time = (20 / 3600.0 * 2) + (1 / 3600 * 6) - assert pytest.approx(env.now) == expected_time - assert len(mgr.debug_log) == 19 - expect = ["Can't send, waiting"] * 5 + ["Stopped trying to send"] + ["Moved message"] * 2 - expect += ["Can't send, waiting"] * 5 + ["Stopped trying to send"] - expect += ["Moved message"] * 4 + ["Destination reached"] - assert [x["event"] for x in mgr.debug_log] == expect - assert nodes["C"].messages.items[0].content.message == "Second message" - - -def test_routing_drop_node_global() -> None: - """When we drop a node, expect it to take longer. - - This has global on, which means B will be tried only once. - """ - with EnvironmentContext() as env: - nodes, mgr = _build_net() - mgr.global_ignore = True - mgr.blocked_nodes.append(nodes["B"]) - mgr.run() - env.process(_start("Third message", mgr, nodes, "A", "C")) - env.run() - expected_time = 20 / 3600.0 + (1 / 3600 * 6) - assert pytest.approx(env.now) == expected_time - assert len(mgr.debug_log) == 13 - expect = ["Can't send, waiting"] * 5 + ["Stopped trying to send"] + ["Moved message"] * 2 - expect += ["Moved message"] * 4 + ["Destination reached"] - assert [x["event"] for x in mgr.debug_log] == expect - assert nodes["C"].messages.items[0].content.message == "Third message" - - -def test_routing_drop_and_return() -> None: - """Drop a node, but bring it back mid-send.""" - with EnvironmentContext() as env: - nodes, mgr = _build_net() - mgr.blocked_nodes.append(nodes["B"]) - mgr.run() - env.process(_start("Fourth message", mgr, nodes, "A", "C")) - - env.run(until=7 / 3600.0) - mgr.blocked_nodes = [] - env.run() - expected_time = 8 / 3600.0 + (1 / 3600 * 2) - assert pytest.approx(env.now) == expected_time - assert len(mgr.debug_log) == 5 - expect = ["Can't send, waiting"] * 2 + ["Moved message"] * 2 + ["Destination reached"] - assert [x["event"] for x in mgr.debug_log] == expect - assert nodes["C"].messages.items[0].content.message == "Fourth message" - - -def test_routing_no_route() -> None: - """Drop a node, but bring it back mid-send.""" - with EnvironmentContext() as env: - nodes, mgr = _build_net() - mgr.blocked_nodes.append(nodes["B"]) - mgr.blocked_nodes.append(nodes["D"]) - mgr.run() - env.process(_start("Fifth message", mgr, nodes, "A", "C")) - - env.run() - expected_time = 20 / 3600.0 * 2 - assert pytest.approx(env.now) == expected_time - assert len(mgr.debug_log) == 13 - expect = ["Can't send, waiting"] * 5 + ["Stopped trying to send"] - expect += ["Can't send, waiting"] * 5 + ["Stopped trying to send"] - expect += ["No message route available"] - assert [x["event"] for x in mgr.debug_log] == expect - assert nodes["C"].messages.items == [] - - -if __name__ == "__main__": - test_routing_no_route() diff --git a/src/upstage_des/test/test_container.py b/src/upstage_des/test/test_container.py deleted file mode 100644 index d54229f..0000000 --- a/src/upstage_des/test/test_container.py +++ /dev/null @@ -1,278 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from random import uniform - -import pytest - -from upstage_des.base import EnvironmentContext -from upstage_des.resources.container import ( - ContainerEmptyError, - ContainerError, - ContinuousContainer, -) -from upstage_des.resources.monitoring import ( - SelfMonitoringContainer, - SelfMonitoringContinuousContainer, -) -from upstage_des.type_help import SIMPY_GEN - -CONTAINER_CAPACITY = 100 -INITIAL_LEVEL = 50 - - -def test_basics() -> None: - with EnvironmentContext() as env: - con = ContinuousContainer( - capacity=CONTAINER_CAPACITY, - env=env, - init=INITIAL_LEVEL, - ) - - assert con.capacity == CONTAINER_CAPACITY - assert con.level == INITIAL_LEVEL - - env.run(until=10) - con._set_new_rate(-10.0) - - with pytest.raises(ContainerEmptyError) as err: - env.run(until=30) - assert err.value.cause == "Container is empty!" - - -def test_error() -> None: - with EnvironmentContext() as env: - con = ContinuousContainer( - capacity=CONTAINER_CAPACITY, - env=env, - init=INITIAL_LEVEL, - ) - con._set_new_rate(-20) - with pytest.raises(ContainerError): - env.run(until=30) - - with pytest.raises(ValueError): - con.put(-20, 3) - - -def test_calculations() -> None: - with EnvironmentContext() as env: - con = ContinuousContainer( - capacity=CONTAINER_CAPACITY, - env=env, - init=INITIAL_LEVEL, - ) - con._set_new_rate(-20.0) - - assert con.time_until_level(0.0) == 2.5 - assert con.time_until_level(CONTAINER_CAPACITY) == float("inf") - - -def test_checking() -> None: - with EnvironmentContext() as env: - auto_started = ContinuousContainer( - capacity=CONTAINER_CAPACITY, - env=env, - init=INITIAL_LEVEL, - ) - auto_started._set_new_rate(-20.0) - env.run(until=2) - assert auto_started.level < INITIAL_LEVEL - - with pytest.raises(ContainerEmptyError): - env.run(until=10) - - -def test_get_and_put() -> None: - with EnvironmentContext() as env: - tank = ContinuousContainer(env, capacity=1000.0, init=500.0) - get_time = 10.0 - get_rate = 50.0 - tank.get(rate=get_rate, time=get_time) - - put_time = 12.0 - put_rate = 60.0 - tank.put(rate=put_rate, time=put_time) - - env.run(until=9) - assert len(tank._active_puts) == 1 - assert len(tank._active_gets) == 1 - - added = -get_rate * 9 + put_rate * 9 - assert tank.level == 500 + added - - env.run(until=11) - assert len(tank._active_puts) == 1 - assert len(tank._active_gets) == 0 - - env.run(until=13) - added = -get_rate * get_time + put_rate * put_time - assert tank.level == 720 - - -def test_interrupting() -> None: - with EnvironmentContext() as env: - tank = ContinuousContainer( - env, - capacity=1000.0, - init=500.0, - error_empty=False, - error_full=False, - ) - - msg = [] - - def full_callback() -> None: - msg.append("full") - - putter = tank.put(10.0, 100.0, custom_callbacks=[full_callback]) - env.run(until=51) - - assert len(msg) == 1 - assert putter not in tank._active_users - - with EnvironmentContext() as env: - tank = ContinuousContainer( - env, - capacity=1000.0, - init=500.0, - error_empty=False, - error_full=False, - ) - - putter = tank.put(10.0, 100.0) - env.run(until=25) - assert tank.level == 500 + 25 * 10 - assert putter in tank._active_users - putter.cancel() - env.run() - assert tank.level == 500 + 25 * 10 - assert putter not in tank._active_users - - -def test_complex_behavior() -> None: - times = [] - tanker_called = [False] - - with EnvironmentContext() as env: - tank = SelfMonitoringContinuousContainer( - env=env, - capacity=1000.0, - init=850.0, - ) - tank._set_new_rate(-1.0) - - d: list[str] = [] - - def tank_is_empty() -> None: - d.append("empty") - - def tank_is_overflowing() -> None: - d.append("overflowing") - - def call_refill() -> SIMPY_GEN: - yield env.timeout(5.0) - - rate = 10.0 - until = min(0.95 * tank.time_until_done(rate=rate), 5.0) - - tank.put(rate=rate, time=until, custom_callbacks=[tank_is_overflowing]) - tanker_called[0] = False - yield env.timeout(until) - - def draw_fuel() -> SIMPY_GEN: - while True: - wait = uniform(10.0, 15.0) - - yield env.timeout(wait) - - rate = uniform(1.0, 2.0) - until = 0.5 * min(tank.time_until_done(rate * 0.99), uniform(20.0, 30.0)) - # print(f"{env.now:5.1f} - Random Draw (rate: {rate:.1f}, " - # f"until={until:.1f})") - tank.get(rate=rate, time=until) - yield env.timeout(until) - # print("{:5.1f} - Random Draw Stopped".format(env.now, rate, - # until)) - - def start_stop_getting() -> SIMPY_GEN: - getter = tank.get(rate=1.0, time=1_000, custom_callbacks=[tank_is_empty]) - # print("{:5.1f} - Started constant getting".format(env.now)) - while True: - wait = uniform(40.0, 60.0) - yield env.timeout(wait) - # print(f"{env.now:5.1f} - Stopped constant getting") - - getter.process.interrupt("stop") - - wait = uniform(40.0, 60.0) - yield env.timeout(wait) - - getter = tank.get(rate=10.0, time=1_000, custom_callbacks=[tank_is_empty]) - # print("{:5.1f} - Restarted constant getting".format(env.now)) - - def simulate() -> SIMPY_GEN: - env.process(draw_fuel()) - env.process(start_stop_getting()) - while True: - times.append(env.now) - t = tank.time_until_level(0.4 * tank.capacity) - if t == float("inf"): - t = tank.time_until_level(tank.capacity) - if t == float("inf"): - t = tank.time_until_level(0.1 * tank.capacity) - t = 1 if t == float("inf") else t - yield env.timeout(t) - if not tanker_called[0] and tank.level <= 0.5 * tank.capacity: - tanker_called[0] = True - env.process(call_refill()) - yield env.timeout(1.0) - - env.process(simulate()) - env.run(until=120) - tank._set_level() - - -def test_basics_monitoring() -> None: - with EnvironmentContext() as env: - con = SelfMonitoringContinuousContainer( - capacity=CONTAINER_CAPACITY, - env=env, - init=INITIAL_LEVEL, - ) - - assert con._capacity == CONTAINER_CAPACITY - assert con.level == INITIAL_LEVEL - - env.run(until=10) - con._set_new_rate(-10.0) - - with pytest.raises(ContainerEmptyError): - env.run(until=30) - - assert len(con._quantities) == 3 - - -def test_checking_monitoring() -> None: - with EnvironmentContext() as env: - auto_started = SelfMonitoringContinuousContainer( - capacity=CONTAINER_CAPACITY, - env=env, - init=INITIAL_LEVEL, - ) - auto_started._add_rate(-20.0) - env.run(until=2) - assert auto_started.level < INITIAL_LEVEL - - with pytest.raises(ContainerEmptyError): - env.run(until=10) - - -def test_self_monitoring_container() -> None: - """Test self-monitoring container""" - with EnvironmentContext() as env: - con = SelfMonitoringContainer(env=env, capacity=CONTAINER_CAPACITY, init=INITIAL_LEVEL) - - assert len(con._quantities) == 1 diff --git a/src/upstage_des/test/test_data_reporting.py b/src/upstage_des/test/test_data_reporting.py deleted file mode 100644 index 2513260..0000000 --- a/src/upstage_des/test/test_data_reporting.py +++ /dev/null @@ -1,308 +0,0 @@ -"""Test the data recording/reporting capabilities.""" - -from collections import Counter -from dataclasses import dataclass - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.data_utils import ( - create_location_table, - create_table, - get_recorded_data, - record_data, -) -from upstage_des.type_help import SIMPY_GEN - - -@dataclass -class Information: - value_1: int - value_2: float - - -class Cashier(UP.Actor): - items_scanned = UP.State[int](recording=True) - other = UP.State[float]() - cue = UP.State[UP.SelfMonitoringStore]() - cue2 = UP.ResourceState[UP.SelfMonitoringContainer](default=UP.SelfMonitoringContainer) - time_working = UP.LinearChangingState(default=0.0, recording=True, record_duplicates=True) - info = UP.State[Information](recording=True) - dicttype = UP.DictionaryState[int](recording=True) - nrdt = UP.DictionaryState[str]() - dc_state = UP.DataclassState[Information](valid_types=Information, recording=True) - - -class Cart(UP.Actor): - location = UP.CartesianLocationChangingState(recording=True) - location_two = UP.CartesianLocationChangingState(recording=True) - holding = UP.State[float](default=0.0, recording=True) - some_data = UP.State[dict[str, float]](recording=True) - - -def test_data_reporting() -> None: - with UP.EnvironmentContext() as env: - t = UP.Task() - cash = Cashier( - name="Ertha", - other=0.0, - items_scanned=0, - cue=UP.SelfMonitoringStore(env), - info=Information(0, 0), - dicttype={"Coupons": 0}, - nrdt={"Special": 4}, - dc_state=Information(1, 1.0), - ) - - cash2 = Cashier( - name="Bertha", - other=0.0, - items_scanned=0, - cue=UP.SelfMonitoringStore(env), - info=Information(1, 1), - dicttype={"Coupons": 0}, - nrdt={"Special": 3}, - dc_state=Information(2, 2.0), - ) - store = UP.SelfMonitoringFilterStore(env, name="Store Test") - cart = Cart( - name="Wobbly Wheel", - location=UP.CartesianLocation(1.0, 1.0), - location_two=UP.CartesianLocation(1.0, 1.0), - some_data={"exam": 2.0}, - ) - - for c in [cash, cash2]: - c.items_scanned += 1 - c.cue.put("A") - c.cue2.put(10) - c.time_working = 0.0 - cash.other = 3.0 - - cart.activate_location_state( - state="location", - speed=2.0, - waypoints=[UP.CartesianLocation(7.0, 6.0)], - task=t, - ) - cart.activate_location_state( - state="location_two", - speed=2.0, - waypoints=[UP.CartesianLocation(-7.0, -6.0)], - task=t, - ) - - env.run(until=0.1) - for c in [cash, cash2]: - c.activate_linear_state( - state="time_working", - rate=1.0, - task=t, - ) - c.dicttype["Coupons"] += 1 - - env.run(until=1) - cart.location - cart.location_two - cash.items_scanned += 2 - store.put("XYZ") - cash.info.value_1 = 3 - cash.record_state("info") - cash2.info.value_2 = 4.3 - cash2.record_state("info") - - cash.dc_state.value_1 += 1 - cash2.dc_state.value_1 += 2 - - for c in [cash, cash2]: - c.cue.put("B") - c.cue2.put(3) - c.time_working - - env.run(until=2) - cart.location - cash.items_scanned += 1 - env.run(until=3) - cart.location - cart.location_two - cart.some_data["exam"] = 4.0 - cart.record_state("some_data") - - cart.deactivate_state(state="location", task=t) - cart.deactivate_state(state="location_two", task=t) - - cash2.deactivate_state(state="time_working", task=t) - - for c in [cash, cash2]: - c.cue.get() - c.cue2.get(2) - c.time_working - - cash.items_scanned = -1 - env.run(until=3.3) - cart.location - cart.location_two = UP.CartesianLocation(-1.0, -1.0) - cart.some_data["new"] = 123.45 - cart.record_state("some_data") - store.put("ABC") - env.run() - cart.location - cart.location_two - for c in [cash, cash2]: - c.time_working - - state_table, cols = create_table() - all_state_table, all_cols = create_table(skip_locations=False) - loc_state_table, loc_cols = create_location_table() - new_table, _ = create_table(skip_locations=True, save_static=True) - orig_table, _ = create_table(skip_locations=True, save_static=False) - - ctr = Counter([row[:3] for row in state_table]) - assert ctr[("Ertha", "Cashier", "items_scanned")] == 5 - assert ctr[("Ertha", "Cashier", "cue")] == 4 - assert ctr[("Ertha", "Cashier", "cue2")] == 4 - assert ctr[("Ertha", "Cashier", "time_working")] == 6 - assert ctr[("Bertha", "Cashier", "items_scanned")] == 2 - assert ctr[("Bertha", "Cashier", "cue")] == 4 - assert ctr[("Bertha", "Cashier", "cue2")] == 4 - assert ctr[("Bertha", "Cashier", "time_working")] == 5 - assert ctr[("Store Test", "SelfMonitoringFilterStore", "Resource")] == 3 - assert ctr[("Ertha", "Cashier", "info.value_1")] == 2 - assert ctr[("Ertha", "Cashier", "info.value_2")] == 2 - assert ctr[("Ertha", "Cashier", "dicttype.Coupons")] == 2 - assert ctr[("Bertha", "Cashier", "dicttype.Coupons")] == 2 - assert ctr[("Bertha", "Cashier", "info.value_1")] == 2 - assert ctr[("Bertha", "Cashier", "info.value_2")] == 2 - assert ctr[("Ertha", "Cashier", "dc_state.value_1")] == 2 - assert ctr[("Bertha", "Cashier", "dc_state.value_1")] == 2 - assert ctr[("Ertha", "Cashier", "dc_state.value_2")] == 1 - assert ctr[("Bertha", "Cashier", "dc_state.value_2")] == 1 - # Test for default values untouched in the sim showing up in the data. - assert ctr[("Wobbly Wheel", "Cart", "holding")] == 1 - assert ctr[("Wobbly Wheel", "Cart", "some_data.exam")] == 3 - assert ctr[("Wobbly Wheel", "Cart", "some_data.new")] == 1 - row = [r for r in state_table if r[:3] == ("Wobbly Wheel", "Cart", "holding")][0] - assert row[4] == 0 - assert row[3] == 0.0 - # Continuing as before - assert len(state_table) == 60 - assert cols == all_cols - assert cols == [ - "Entity Name", - "Entity Type", - "State Name", - "Time", - "Value", - "Activation Status", - ] - - ctr = Counter([row[:3] for row in all_state_table]) - assert ctr[("Ertha", "Cashier", "items_scanned")] == 5 - assert ctr[("Ertha", "Cashier", "cue")] == 4 - assert ctr[("Ertha", "Cashier", "cue2")] == 4 - assert ctr[("Ertha", "Cashier", "time_working")] == 6 - assert ctr[("Bertha", "Cashier", "items_scanned")] == 2 - assert ctr[("Bertha", "Cashier", "cue")] == 4 - assert ctr[("Bertha", "Cashier", "cue2")] == 4 - assert ctr[("Bertha", "Cashier", "time_working")] == 5 - assert ctr[("Store Test", "SelfMonitoringFilterStore", "Resource")] == 3 - assert ctr[("Wobbly Wheel", "Cart", "holding")] == 1 - assert ctr[("Wobbly Wheel", "Cart", "location")] == 4 - assert ctr[("Wobbly Wheel", "Cart", "location_two")] == 4 - assert len(all_state_table) == 38 + 8 + 12 + 4 + 6 - - assert loc_cols == [ - "Entity Name", - "Entity Type", - "State Name", - "Time", - "X", - "Y", - "Z", - "Activation Status", - ] - assert len(loc_state_table) == 8 - assert loc_state_table[-1] == ( - "Wobbly Wheel", - "Cart", - "location_two", - 3.3, - -1.0, - -1.0, - 0.0, - "inactive", - ) - - match1 = ("Ertha", "Cashier", "other", 0.0, 3.0, "Last Seen") - assert match1 in new_table - assert match1 not in all_state_table - - match2 = ("Bertha", "Cashier", "other", 0.0, 0.0, "Last Seen") - assert match2 in new_table - assert match2 not in all_state_table - - match3 = ("Bertha", "Cashier", "nrdt.Special", 0.0, 3, "Last Seen") - match4 = ("Ertha", "Cashier", "nrdt.Special", 0.0, 4, "Last Seen") - assert match3 in new_table - assert match3 not in all_state_table - assert match4 in new_table - assert match4 not in all_state_table - - # Only the two "other" states should show up, and the new DictionaryState - assert len(new_table) - len(orig_table) == 4 - - -def test_store_failure() -> None: - class Exam(UP.Actor): - a_store = UP.ResourceState[SIM.Store](default=SIM.Store) - - with UP.EnvironmentContext() as env: - ex = Exam(name="example") - - def _proc() -> SIMPY_GEN: - yield ex.a_store.put("a thing") - yield env.timeout(1.0) - assert ex.a_store.items == ["a thing"] - yield ex.a_store.get() - - env.process(_proc()) - env.run() - assert env.now == 1 - assert ex.a_store.items == [] - - data, cols = create_table() - assert data == [] - - -def test_data_recorder() -> None: - with UP.EnvironmentContext() as env: - record_data("First") - record_data({"ok": "working"}) - env.run(until=3.0) - record_data(1.23) - env.run(until=4.0) - info = Information(1, 2.0) - record_data(info, copy=True) - record_data(info, copy=False) - info.value_1 = 5 - - the_data = get_recorded_data() - assert len(the_data) == 5 - assert the_data[0] == (0.0, "First") - assert the_data[1] == (0.0, {"ok": "working"}) - assert the_data[2] == (3.0, 1.23) - assert the_data[3][0] == 4.0 - info_stored = the_data[3][1] - assert getattr(info_stored, "value_1", None) == 1 - assert getattr(info_stored, "value_2", None) == 2.0 - assert id(info_stored) != id(info) - - assert the_data[4][0] == 4.0 - info_stored_2 = the_data[4][1] - assert getattr(info_stored_2, "value_1", None) == 5 - assert getattr(info_stored_2, "value_2", None) == 2.0 - assert id(info_stored_2) == id(info) - - -if __name__ == "__main__": - test_data_reporting() diff --git a/src/upstage_des/test/test_data_types.py b/src/upstage_des/test/test_data_types.py deleted file mode 100644 index 8a38332..0000000 --- a/src/upstage_des/test/test_data_types.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from dataclasses import FrozenInstanceError -from math import radians, sqrt - -import pytest - -import upstage_des.api as UP -from upstage_des.geography import Spherical, get_intersection_locations - -STAGE_SETUP = dict( - altitude_units="ft", - distance_units="nmi", - intersection_model=get_intersection_locations, - stage_model=Spherical, -) - - -def test_basics() -> None: - with UP.EnvironmentContext(): - with pytest.raises(NotImplementedError): - l1 = UP.Location() - l1 - 3 - - -def test_hashable() -> None: - with UP.EnvironmentContext(): - p1 = [10, 10] - point_1 = UP.CartesianLocation(*p1) - key = point_1._key() - assert key == (10, 10, 0.0, False) - # assert getattr(point_1, "__hash__") is not None - _ = {point_1: 1.0} - - -def test_cartesian() -> None: - with UP.EnvironmentContext(): - for k, v in STAGE_SETUP.items(): - UP.add_stage_variable(k, v) - - p1 = [10.0, 10.0] - p2 = [1.0, 2.0, 3.0] - origin = UP.CartesianLocation(0, 0) - point_1 = UP.CartesianLocation(*p1) - point_2 = UP.CartesianLocation(*p2) - - assert origin - point_1 == sqrt(200) - assert origin - point_1 == point_1 - origin - - assert point_1 - point_2 > 0 - for index, value in enumerate(p2): - assert point_2[index] == value - - assert "10" in point_1.__repr__() - - with pytest.raises(ValueError): - point_2[3] is None - - with pytest.raises(ValueError): - point_1 - 10 - - with pytest.raises(ValueError): - point_2 == 10 - - point_3 = UP.CartesianLocation(*p2, use_altitude_units=True) - - assert point_3 != point_2 - - point_2a = UP.CartesianLocation(1, 2, 3.000000001) - assert point_2 == point_2a, f"Nearly equal points are still {point_2 - point_2a} too far" - - -def test_geodetic() -> None: - with UP.EnvironmentContext(): - for k, v in STAGE_SETUP.items(): - UP.add_stage_variable(k, v) - - lat, lon, alt = 33, -86, 1000 - loc_up = UP.GeodeticLocation(lat, lon, alt) - - with pytest.raises(FrozenInstanceError): - loc_up.alt = 5_000 - - loc_up_rad = loc_up.to_radians() - assert loc_up_rad.lat == radians(loc_up.lat) - assert loc_up_rad.lon == radians(loc_up.lon) - assert loc_up_rad.alt == loc_up.alt - - loc_up_rad_1 = UP.GeodeticLocation( - lat=radians(loc_up.lat), - lon=radians(loc_up.lon), - in_radians=True, - ) - assert loc_up_rad_1.lat == loc_up_rad.lat - assert loc_up_rad_1.lon == loc_up_rad.lon - - assert loc_up_rad == loc_up_rad.to_radians() - assert loc_up == loc_up.to_degrees() - - assert loc_up_rad == loc_up - assert loc_up == loc_up_rad - - assert loc_up_rad - UP.GeodeticLocation(10, 10) > 0 - assert UP.GeodeticLocation(10, 10) - loc_up_rad > 0 - - -def test_data_objects() -> None: - cart1 = UP.CartesianLocationData(1.0, 2.1, 3.2) - cart2 = UP.CartesianLocationData(1.0, 2.1, 3.2) - assert cart1 == cart2 - - with pytest.raises(ValueError): - cart1 == (1.0, 2.1, 3.2) - - geo1 = UP.GeodeticLocationData(13.0, 12.1, 11.2) - geo2 = UP.GeodeticLocationData(13.0, 12.1, 11.2) - assert geo1 == geo2 - - geo3 = UP.GeodeticLocationData(radians(13.0), radians(12.1), 11.2, in_radians=True) - assert geo1 == geo3 - assert geo3 == geo1 - - with pytest.raises(ValueError): - geo1 == (13.0, 12.1, 11.2) - - with UP.EnvironmentContext(): - for k, v in STAGE_SETUP.items(): - UP.add_stage_variable(k, v) - - loc1 = geo1.make_location() - assert isinstance(loc1, UP.GeodeticLocation) - - loc3 = geo3.make_location() - - assert loc1 - loc3 == 0.0 - assert loc1 == loc3 - - loc1 = cart1.make_location() - loc2 = cart2.make_location() - assert loc1 == loc2 diff --git a/src/upstage_des/test/test_docs_examples/__init__.py b/src/upstage_des/test/test_docs_examples/__init__.py deleted file mode 100644 index a73c948..0000000 --- a/src/upstage_des/test/test_docs_examples/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. diff --git a/src/upstage_des/test/test_docs_examples/test_cashier.py b/src/upstage_des/test/test_docs_examples/test_cashier.py deleted file mode 100644 index d039ad8..0000000 --- a/src/upstage_des/test/test_docs_examples/test_cashier.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from collections.abc import Generator - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - - -class Cashier(UP.Actor): - scan_speed = UP.State[float]( - valid_types=(float,), - frozen=True, - ) - time_until_break = UP.State[float]( - default=120.0, - valid_types=(float,), - frozen=True, - ) - breaks_until_done = UP.State[int](default=2, valid_types=int) - breaks_taken = UP.State[int](default=0, valid_types=int, recording=True) - items_scanned = UP.State[int]( - default=0, - valid_types=(int,), - recording=True, - ) - time_scanning = UP.LinearChangingState( - default=0.0, - valid_types=(float,), - ) - - -class CheckoutLane(UP.Actor): - customer_queue = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, - ) - - -class StoreBoss(UP.UpstageBase): - def __init__(self, lanes: list[CheckoutLane]) -> None: - self.lanes = lanes - self._lane_map: dict[CheckoutLane, Cashier] = {} - - def get_lane(self, cashier: Cashier) -> CheckoutLane: - possible = [lane for lane in self.lanes if lane not in self._lane_map] - lane = self.stage.random.choice(possible) - self._lane_map[lane] = cashier - return lane - - -class GoToWork(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Go to work""" - yield UP.Wait(15.0) - - -class TalkToBoss(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Zero-time task to get information.""" - boss: StoreBoss = self.stage.boss - lane = boss.get_lane(actor) - self.set_actor_knowledge(actor, "checkout_lane", lane, overwrite=False) - actor.breaks_taken = 0 - self.set_actor_knowledge(actor, "start_time", self.env.now) - - -class WaitInLane(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Wait until break time, or a customer.""" - lane: CheckoutLane = self.get_actor_knowledge( - actor, - "checkout_lane", - must_exist=True, - ) - customer_arrival = UP.Get(lane.customer_queue) - - start_time = self.get_actor_knowledge( - actor, - "start_time", - must_exist=True, - ) - break_start = start_time + actor.time_until_break - wait_until_break = break_start - self.env.now - if wait_until_break < 0: - self.set_actor_task_queue(actor, ["Break"]) - return - - break_event = UP.Wait(wait_until_break) - - yield UP.Any(customer_arrival, break_event) - - if customer_arrival.is_complete(): - customer: int = customer_arrival.get_value() - self.set_actor_knowledge(actor, "customer", customer, overwrite=True) - else: - customer_arrival.cancel() - self.set_actor_task_queue(actor, ["Break"]) - - -class DoCheckout(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Do the checkout""" - items: int = self.get_actor_knowledge( - actor, - "customer", - must_exist=True, - ) - per_item_time = actor.scan_speed / items - actor.activate_linear_state( - state="time_scanning", - rate=1.0, - task=self, - ) - for _ in range(items): - yield UP.Wait(per_item_time) - actor.items_scanned += 1 - actor.deactivate_all_states(task=self) - # assume 2 minutes to take payment - yield UP.Wait(2.0) - - -class Break(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Decide what kind of break we are taking.""" - actor.breaks_taken += 1 - if actor.breaks_taken == actor.breaks_until_done: - self.set_actor_task_queue(actor, ["NightBreak"]) - elif actor.breaks_taken > actor.breaks_until_done: - raise UP.SimulationError("Too many breaks taken") - else: - self.set_actor_task_queue(actor, ["ShortBreak"]) - - -class ShortBreak(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Take a short break.""" - yield UP.Wait(15.0) - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - - -class NightBreak(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Go home and rest.""" - self.clear_actor_knowledge(actor, "checkout_lane") - yield UP.Wait(60 * 12.0) - - -task_classes = { - "GoToWork": GoToWork, - "TalkToBoss": TalkToBoss, - "WaitInLane": WaitInLane, - "DoCheckout": DoCheckout, - "Break": Break, - "ShortBreak": ShortBreak, - "NightBreak": NightBreak, -} - -task_links = { - "GoToWork": UP.TaskLinks(default="TalkToBoss", allowed=["TalkToBoss"]), - "TalkToBoss": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "WaitInLane": UP.TaskLinks(default="DoCheckout", allowed=["DoCheckout", "Break"]), - "DoCheckout": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "Break": UP.TaskLinks(default="ShortBreak", allowed=["ShortBreak", "NightBreak"]), - "ShortBreak": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "NightBreak": UP.TaskLinks(default="GoToWork", allowed=["GoToWork"]), -} - - -cashier_task_network = UP.TaskNetworkFactory( - name="CashierJob", - task_classes=task_classes, - task_links=task_links, -) - - -def customer_spawner( - env: SIM.Environment, - lanes: list[CheckoutLane], -) -> Generator[SIM.Event, None, None]: - # sneaky way to get access to stage - stage = lanes[0].stage - while True: - hrs = env.now / 60 - time_of_day = hrs // 24 - if time_of_day <= 8 or time_of_day >= 15.5: - time_until_open = (24 - time_of_day) + 8 - yield env.timeout(time_until_open) - - lane_pick = stage.random.choice(lanes) - number_pick = stage.random.randint(3, 17) - yield lane_pick.customer_queue.put(number_pick) - yield UP.Wait.from_random_uniform(5.0, 30.0).as_event() - - -def test_cashier_example() -> None: - with UP.EnvironmentContext(initial_time=8 * 60) as env: - UP.add_stage_variable("time_unit", "min") - cashier = Cashier( - name="Bob", - scan_speed=1.0, - time_until_break=120.0, - breaks_until_done=4, - debug_log=True, - ) - lane_1 = CheckoutLane(name="Lane 1") - lane_2 = CheckoutLane(name="Lane 2") - boss = StoreBoss(lanes=[lane_1, lane_2]) - - UP.add_stage_variable("boss", boss) - - net = cashier_task_network.make_network() - cashier.add_task_network(net) - cashier.start_network_loop(net.name, "GoToWork") - - customer_proc = customer_spawner(env, [lane_1, lane_2]) - _ = env.process(customer_proc) - - env.run(until=20 * 60) - - for line in cashier.get_log(): - print(line) - - -if __name__ == "__main__": - test_cashier_example() diff --git a/src/upstage_des/test/test_docs_examples/test_cashier_complex.py b/src/upstage_des/test/test_docs_examples/test_cashier_complex.py deleted file mode 100644 index 4cb28d9..0000000 --- a/src/upstage_des/test/test_docs_examples/test_cashier_complex.py +++ /dev/null @@ -1,340 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for license terms. - -from collections.abc import Generator -from typing import Any - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.task import InterruptStates -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - -BREAK_TIME = 15.0 - - -class Cashier(UP.Actor): - scan_speed = UP.State[float]( - valid_types=(float,), - frozen=True, - ) - time_until_break = UP.State[float]( - default=120.0, - valid_types=(float,), - frozen=True, - ) - breaks_until_done = UP.State[int](default=2, valid_types=int) - breaks_taken = UP.State[int](default=0, valid_types=int, recording=True) - items_scanned = UP.State[int]( - default=0, - valid_types=(int,), - recording=True, - ) - time_scanning = UP.LinearChangingState( - default=0.0, - valid_types=(float,), - ) - messages = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, - ) - current_task = UP.State[str](default="init", recording=True) - - def time_left_to_break(self) -> float: - elapsed = self.env.now - float(self.get_knowledge("start_time", must_exist=True)) - return self.time_until_break - elapsed - - -class CheckoutLane(UP.Actor): - customer_queue = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, - ) - - -class StoreBoss(UP.UpstageBase): - def __init__(self, lanes: list[CheckoutLane]) -> None: - self.lanes = lanes - self._lane_map: dict[CheckoutLane, Cashier] = {} - - def get_lane(self, cashier: Cashier) -> CheckoutLane: - possible = [lane for lane in self.lanes if lane not in self._lane_map] - lane = self.stage.random.choice(possible) - self._lane_map[lane] = cashier - return lane - - def clear_lane(self, cashier: Cashier) -> None: - to_del = [name for name, cash in self._lane_map.items() if cash is cashier] - for name in to_del: - del self._lane_map[name] - - -class CashierBreakTimer(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - times = [ - self.env.now + actor.time_until_break * b for b in range(1, actor.breaks_until_done + 1) - ] - for t in times: - yield UP.Wait(t - self.env.now) - actor.interrupt_network("CashierJob", cause=dict(reason="BREAK TIME")) - - -class InterruptibleTask(UP.Task): - def on_interrupt(self, *, actor: Cashier, cause: dict[str, Any]) -> InterruptStates: - # We will only interrupt with a dictionary of data - assert isinstance(cause, dict) - job_list: list[str] - - if cause["reason"] == "BREAK TIME": - job_list = ["Break"] - elif cause["reason"] == "NEW JOB": - job_list = cause["job_list"] - else: - raise UP.SimulationError("Unexpected interrupt cause") - - # determine time until break - time_left = actor.time_left_to_break() - # if there are only five minutes left, take the break and queue the task. - if time_left <= 5.0 and "Break" not in job_list: - job_list = ["Break"] + job_list - - # Ignore the interrupt, unless we've marked it to know otherwise - marker = self.get_marker() or "none" - if marker == "on break" and "Break" in job_list: - job_list.remove("Break") - - self.clear_actor_task_queue(actor) - self.set_actor_task_queue(actor, job_list) - if marker == "cancellable": - return self.INTERRUPT.END - return self.INTERRUPT.IGNORE - - -class GoToWork(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Go to work""" - actor.current_task = "Going to Work" - yield UP.Wait(15.0) - - -class TalkToBoss(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Zero-time task to get information.""" - actor.current_task = "Talking to Boss" - boss: StoreBoss = self.stage.boss - lane = boss.get_lane(actor) - self.set_actor_knowledge(actor, "checkout_lane", lane, overwrite=False) - actor.breaks_taken = 0 - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - # Convenient spot to run the timer. - CashierBreakTimer().run(actor=actor) - - -class WaitInLane(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Wait until break time, or a customer.""" - actor.current_task = "Waiting for Customer" - lane: CheckoutLane = self.get_actor_knowledge( - actor, - "checkout_lane", - must_exist=True, - ) - customer_arrival = UP.Get(lane.customer_queue) - - self.set_marker(marker="cancellable") - yield customer_arrival - - customer: int = customer_arrival.get_value() - self.set_actor_knowledge(actor, "customer", customer, overwrite=True) - - -class DoCheckout(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Do the checkout""" - actor.current_task = "Checking out a Customer" - items: int = self.get_actor_knowledge( - actor, - "customer", - must_exist=True, - ) - per_item_time = actor.scan_speed / items - actor.activate_linear_state( - state="time_scanning", - rate=1.0, - task=self, - ) - for _ in range(items): - yield UP.Wait(per_item_time) - actor.items_scanned += 1 - actor.deactivate_all_states(task=self) - # assume 2 minutes to take payment - yield UP.Wait(2.0) - - -class Break(UP.DecisionTask): - def make_decision(self, *, actor: Cashier) -> None: - """Decide what kind of break we are taking.""" - actor.breaks_taken += 1 - - # we might have jobs queued - queue = self.get_actor_task_queue(actor) or [] - if "Break" in queue: - raise UP.SimulationError("Odd task network state") - self.clear_actor_task_queue(actor) - - if actor.breaks_taken == actor.breaks_until_done: - self.set_actor_task_queue(actor, ["NightBreak"]) - elif actor.breaks_taken > actor.breaks_until_done: - raise UP.SimulationError("Too many breaks taken") - else: - self.set_actor_task_queue(actor, ["ShortBreak"] + queue) - - -class ShortBreak(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Take a short break.""" - actor.current_task = "On Short Break" - self.set_marker("on break") - yield UP.Wait(BREAK_TIME) - self.set_actor_knowledge(actor, "start_time", self.env.now, overwrite=True) - - -class NightBreak(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Go home and rest.""" - actor.current_task = "Home for the Night" - self.clear_actor_knowledge(actor, "checkout_lane") - self.stage.boss.clear_lane(actor) - yield UP.Wait(60 * 12.0) - - -class Restock(InterruptibleTask): - def task(self, *, actor: Cashier) -> TASK_GEN: - """Restock.""" - actor.current_task = "Restock" - self.set_marker("quick task") - yield UP.Wait(10.0) - - -task_classes = { - "GoToWork": GoToWork, - "TalkToBoss": TalkToBoss, - "WaitInLane": WaitInLane, - "DoCheckout": DoCheckout, - "Break": Break, - "ShortBreak": ShortBreak, - "NightBreak": NightBreak, - "Restock": Restock, -} - -task_links = { - "GoToWork": UP.TaskLinks(default="TalkToBoss", allowed=["TalkToBoss"]), - "TalkToBoss": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "WaitInLane": UP.TaskLinks(default="DoCheckout", allowed=["DoCheckout", "Break"]), - "DoCheckout": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane", "Break"]), - "Break": UP.TaskLinks(default="ShortBreak", allowed=["ShortBreak", "NightBreak"]), - "ShortBreak": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane"]), - "NightBreak": UP.TaskLinks(default="GoToWork", allowed=["GoToWork"]), - "Restock": UP.TaskLinks(default="WaitInLane", allowed=["WaitInLane", "Break"]), -} - -cashier_task_network = UP.TaskNetworkFactory( - name="CashierJob", - task_classes=task_classes, - task_links=task_links, -) - - -class CashierMessages(UP.Task): - def task(self, *, actor: Cashier) -> TASK_GEN: - getter = UP.Get(actor.messages) - yield getter - tasks_needed: list[str] | str = getter.get_value() - tasks_needed = [tasks_needed] if isinstance(tasks_needed, str) else tasks_needed - actor.interrupt_network("CashierJob", cause=dict(reason="NEW JOB", job_list=tasks_needed)) - - -cashier_message_net = UP.TaskNetworkFactory.from_single_looping("Messages", CashierMessages) - - -def customer_spawner( - env: SIM.Environment, - lanes: list[CheckoutLane], - max_wait: float = 30.0, -) -> Generator[SIM.Event, None, None]: - # sneaky way to get access to stage - stage = lanes[0].stage - t_until = (8 * 60 + 1) - env.now - t_until = max(t_until, 0.0) - yield env.timeout(t_until) - while True: - hrs = env.now / 60 - days = hrs // 24 - time_of_day = hrs % 24 - if time_of_day >= 18.5: - time_at_open = 24 * (days + 1) + 8 - mins_to_open = (time_at_open - hrs) * 60 - yield env.timeout(mins_to_open) - - lane_pick = stage.random.choice(lanes) - number_pick = stage.random.randint(3, 17) - yield lane_pick.customer_queue.put(number_pick) - yield UP.Wait.from_random_uniform(5.0, max_wait).as_event() - - -def manager_process(boss: StoreBoss, cashiers: list[Cashier]) -> SIMPY_GEN: - while True: - # Use the random uniform feature, but convert the UPSTAGE event to simpy - # because this is a simpy only process - yield UP.Wait.from_random_uniform(30.0, 90.0).as_event() - possible = [ - cash - for cash in cashiers - if getattr(cash.get_running_task("CashierJob"), "name", "") != "NightBreak" - ] - if not possible: - return - cash = boss.stage.random.choice(possible) - yield cash.messages.put(["Restock"]) - - -def test_cashier_example() -> None: - with UP.EnvironmentContext(initial_time=8 * 60) as env: - UP.add_stage_variable("time_unit", "min") - cashier = Cashier( - name="Bob", - scan_speed=1.0, - time_until_break=120.0, - breaks_until_done=4, - debug_log=True, - ) - lane_1 = CheckoutLane(name="Lane 1") - lane_2 = CheckoutLane(name="Lane 2") - boss = StoreBoss(lanes=[lane_1, lane_2]) - - UP.add_stage_variable("boss", boss) - - net = cashier_task_network.make_network() - cashier.add_task_network(net) - cashier.start_network_loop(net.name, "GoToWork") - - net = cashier_message_net.make_network() - cashier.add_task_network(net) - cashier.start_network_loop(net.name, "CashierMessages") - - customer_proc = customer_spawner(env, [lane_1, lane_2]) - _ = env.process(customer_proc) - - _ = env.process(manager_process(boss, [cashier])) - - env.run(until=20 * 60) - - for line in cashier.get_log(): - if "Interrupt" in line: - print(line) - - print(cashier.items_scanned) - - -if __name__ == "__main__": - test_cashier_example() diff --git a/src/upstage_des/test/test_docs_examples/test_nucleus_sharing.py b/src/upstage_des/test/test_docs_examples/test_nucleus_sharing.py deleted file mode 100644 index e0d8d3b..0000000 --- a/src/upstage_des/test/test_docs_examples/test_nucleus_sharing.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class CPU(UP.Actor): - n_procs = UP.State[int](default=0, valid_types=int, recording=True) - jobs = UP.ResourceState[SIM.Store](default=SIM.Store) - - @staticmethod - def time_from_data(process_data: dict[str, float]) -> float: - best_time = process_data["best time"] - percent_complete = process_data["percent complete"] - alloc = process_data["allocated"] - work_left = best_time * (1 - percent_complete) - time = work_left / alloc - return time - - @staticmethod - def left_from_partial( - process_data: dict[str, float], start_time: float, curr_time: float - ) -> float: - alloc = process_data["allocated"] - work_left = process_data["best time"] * (1 - process_data["percent complete"]) - elapsed = curr_time - start_time - percent_of_left = 1 - (elapsed / (work_left / alloc)) - percent_left = (1 - process_data["percent complete"]) * percent_of_left - return percent_left - - -class CPUProcessStart(UP.DecisionTask): - def get_name(self) -> str: - return f"{self._network_name}" - - def make_decision(self, *, actor: CPU) -> None: - knowledge_name = self.get_name() - process_data: dict[str, float] = self.get_actor_knowledge( - actor, knowledge_name, must_exist=True - ) - # the task takes some amount of time to finish based on its cpu amount - assert process_data["percent complete"] == 0.0 - # Touch the nucleus variable before it affects this network - actor.n_procs += 1 - # Now we can add ourselves to the nucleus - nucleus = actor.get_nucleus() - nucleus.add_network(knowledge_name, ["n_procs"]) - - -class CPUProcess(UP.Task): - def get_name(self) -> str: - return f"{self._network_name}" - - def task(self, *, actor: CPU) -> TASK_GEN: - knowledge_name = self.get_name() - process_data: dict[str, float] = self.get_actor_knowledge( - actor, knowledge_name, must_exist=True - ) - # We know at this point we're part of the n_procs - allocate_amount = 1 / (actor.n_procs) - process_data["allocated"] = allocate_amount - self.set_actor_knowledge(actor, knowledge_name, process_data, overwrite=True) - self.set_marker("RUNNING") - time = actor.time_from_data(process_data) - print( - f"{self.env.now:.2f}: Starting: {knowledge_name}\n\tAllocated: {allocate_amount:.2f}" - f"\n\tTime Left: {time:.2f}" - ) - yield UP.Wait(time) - self.clear_actor_knowledge(actor, knowledge_name) - actor.get_nucleus().remove_network(self.get_name()) - actor.n_procs -= 1 - print(f"{self.env.now:.2f}: Done with: {knowledge_name}") - - def on_interrupt(self, *, actor: CPU, cause: str | UP.NucleusInterrupt) -> UP.InterruptStates: - if isinstance(cause, UP.NucleusInterrupt): - assert cause.state_name == "n_procs" - - start_time = self.get_marker_time() - assert start_time is not None - knowledge_name = self.get_name() - process_data: dict[str, float] = self.get_actor_knowledge( - actor, knowledge_name, must_exist=True - ) - perc = actor.left_from_partial(process_data, start_time, self.env.now) - process_data["percent complete"] = perc - self.set_actor_knowledge(actor, knowledge_name, process_data, overwrite=True) - - return self.INTERRUPT.RESTART - raise UP.SimulationError("Unexpected interrupt state") - - -cpu_job_factory = UP.TaskNetworkFactory.from_ordered_terminating( - name="SingleJob", task_classes=[CPUProcessStart, CPUProcess] -) - - -class CPUJobFarmer(UP.Task): - def task(self, *, actor: CPU) -> TASK_GEN: - job = yield UP.Get(actor.jobs) - - suggest = actor.suggest_network_name(cpu_job_factory) - new_net = cpu_job_factory.make_network(other_name=suggest) - actor.add_task_network(new_net) - - proc_know = {"best time": job, "percent complete": 0.0} - self.set_actor_knowledge(actor, suggest, proc_know) - actor.start_network_loop(suggest, init_task_name="CPUProcessStart") - - -cpu_farmer_factory = UP.TaskNetworkFactory.from_single_looping( - name="Dispatch", task_class=CPUJobFarmer -) - - -def test_nucleus_sharing() -> None: - job_time_list = [100.0, 10.0, 15.0, 13.0, 5.0, 25.0] - job_start_delay = [0.0, 3.0, 5.0, 10.0, 10.0, 3.0] - - with UP.EnvironmentContext() as env: - cpu = CPU( - name="Magic Computer", - n_procs=0, - ) - _ = UP.TaskNetworkNucleus(actor=cpu) - - net = cpu_farmer_factory.make_network() - cpu.add_task_network(net) - cpu.start_network_loop(net.name, "CPUJobFarmer") - - def job_sender() -> SIMPY_GEN: - for time, delay in zip(job_time_list, job_start_delay): - yield env.timeout(delay) - yield cpu.jobs.put(time) - - env.process(job_sender()) - env.run() - print(env.now) diff --git a/src/upstage_des/test/test_docs_examples/test_rehearsing_example.py b/src/upstage_des/test/test_docs_examples/test_rehearsing_example.py deleted file mode 100644 index 8c9718d..0000000 --- a/src/upstage_des/test/test_docs_examples/test_rehearsing_example.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN -from upstage_des.utils import waypoint_time_and_dist - - -class Plane(UP.Actor): - speed = UP.State[float]() - location = UP.CartesianLocationChangingState() - fuel = UP.LinearChangingState() - fuel_burn = UP.State[float]() - - -class Fly(UP.Task): - def task(self, *, actor: Plane) -> TASK_GEN: - fly_to: list[UP.CartesianLocation] = self.get_actor_knowledge( - actor, "destination", must_exist=True - ) - time, dist = waypoint_time_and_dist(actor.location, fly_to, actor.speed) - print(f"Rehearsing the task: {self._rehearsing}") - print(f"\tFlying {dist:.2f} units over {time:.2f} hrs") - actor.activate_linear_state( - state="fuel", - rate=-actor.fuel_burn, - task=self, - ) - actor.activate_location_state( - state="location", - speed=actor.speed, - waypoints=fly_to, - task=self, - ) - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - - -class Search(UP.Task): - def task(self, *, actor: Plane) -> TASK_GEN: - search_event = actor.create_knowledge_event( - name="FOUND SURVIVOR", - rehearsal_time_to_complete=0.5, - ) - actor.activate_linear_state( - state="fuel", - rate=-actor.fuel_burn, - task=self, - ) - yield search_event - actor.deactivate_all_states(task=self) - - -class Land(UP.Task): - def task(self, *, actor: Plane) -> TASK_GEN: - # Do a landing of some kind - event = actor.create_knowledge_event(name="DONE", rehearsal_time_to_complete=10.0) - yield event - - -def some_preference_function( - spots: list[UP.CartesianLocation], -) -> UP.CartesianLocation | None: - """Choose a spot. bvv z - - Args: - spots (list[UP.CartesianLocation]): List of spots to search at - - Returns: - UP.CartesianLocation | None: A spot to search or None if we should go home - """ - return spots[0] - - -class Planner(UP.DecisionTask): - def make_decision(self, *, actor: Plane) -> None: - go_to_loc = some_preference_function(self.stage.search_spots) - if go_to_loc is None: # implies we are done with searching - self.set_actor_task_queue(actor, ["Fly", "Land"]) - else: - self.set_actor_knowledge(actor, "destination", go_to_loc, overwrite=True) - self.set_actor_task_queue(actor, ["Fly", "Search"]) - - def rehearse_decision(self, *, actor: Plane) -> None: - # Pop off a destination from the queue, or go "home" - next_dests: list[list[UP.CartesianLocation]] | None = self.get_actor_knowledge( - actor, "destination_plan", must_exist=False - ) - dests: list[UP.CartesianLocation] - task_queue: list[str] - if not next_dests: # fly home - dests = [UP.CartesianLocation(0, 0)] - task_queue = ["Fly", "Land"] - else: # pop a location from the plan - dests = next_dests.pop(0) - self.set_actor_knowledge(actor, "destination_plan", next_dests, overwrite=True) - task_queue = ["Fly", "Search"] - - self.set_actor_knowledge(actor, "destination", dests, overwrite=True) - self.set_actor_task_queue(actor, task_queue) - - -task_classes = {"Fly": Fly, "Search": Search, "Planner": Planner, "Land": Land} -task_links = { - "Fly": UP.TaskLinks(default="Search", allowed=["Fly", "Land", "Search"]), - "Search": UP.TaskLinks(default="Planner", allowed=["Planner"]), - "Planner": UP.TaskLinks(default="Fly", allowed=["Fly"]), - "Land": UP.TaskLinks(default=None, allowed=["Fly"]), -} -search_network = UP.TaskNetworkFactory("SearchNet", task_classes, task_links) - - -def test_model() -> None: - with UP.EnvironmentContext() as env: - search_locs = [ - [UP.CartesianLocation(x, y)] - for x, y in [ - (10, 20), - (30, 10), - (15, 15), - ] - ] - - plane = Plane( - name="searcher", - speed=2, - fuel=200, - fuel_burn=5.0, - location=UP.CartesianLocation(20, 10), - debug_log=True, - ) - net = search_network.make_network() - plane.add_task_network(net) - - new_plane = plane.rehearse_network( - net.name, - task_name_list=["Planner", "Fly", "Search"], - knowledge={"destination_plan": search_locs}, - end_task="Land", - ) - print(f"Fuel left: {new_plane.fuel}") - print(f"Time passed: {new_plane.env.now}") - print(f"Actual time passed: {env.now}") - assert pytest.approx(new_plane.fuel) == 6.18148 - assert pytest.approx(new_plane.env.now) == 38.76370356758358 - assert plane.fuel == 200 - assert env.now == 0 diff --git a/src/upstage_des/test/test_event.py b/src/upstage_des/test/test_event.py deleted file mode 100644 index d0a1a06..0000000 --- a/src/upstage_des/test/test_event.py +++ /dev/null @@ -1,595 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest -import simpy as SIM -from simpy.resources import base -from simpy.resources.container import ContainerGet, ContainerPut -from simpy.resources.store import StoreGet, StorePut - -from upstage_des.api import ( - Actor, - EnvironmentContext, - SimulationError, - State, - Task, - add_stage_variable, -) -from upstage_des.events import ( - All, - Any, - BaseEvent, - BaseRequestEvent, - Event, - Get, - Put, - ResourceHold, - Wait, -) -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -def test_base_event() -> None: - init_time = 1.23 - with EnvironmentContext(initial_time=init_time) as env: - base = BaseEvent() - assert base.created_at == init_time, "Problem in environment time being stored in event" - assert base.env is env, "Problem in environment being stored in event" - - with pytest.raises(NotImplementedError): - base.as_event() - - -def test_wait_event() -> None: - init_time = 1.23 - with EnvironmentContext(initial_time=init_time) as env: - timeout = 1 - - wait = Wait(timeout=timeout) - assert wait.created_at == init_time, "Problem in environment time being stored in event" - assert wait.env is env, "Problem in environment being stored in event" - assert wait.timeout == timeout - - ret = wait.as_event() - assert isinstance(ret, SIM.Timeout), "Wait doesn't return a simpy timeout" - assert ret._delay == timeout, "Incorrect timeout time" - - with EnvironmentContext() as env: - add_stage_variable("time_unit", "minutes") - wait = Wait(timeout=1.1, timeout_unit="hours") - assert wait.timeout == 1.1 * 60 - - with EnvironmentContext(initial_time=init_time) as env: - timeout_2 = [1, 3] - wait = Wait.from_random_uniform(timeout_2[0], timeout_2[1]) - ret = wait.as_event() - assert isinstance(ret, SIM.Timeout), "Wait doesn't return a simpy timeout" - assert timeout_2[0] <= ret._delay <= timeout_2[1], "Incorrect timeout time" - - with pytest.raises(SimulationError): - Wait(timeout={1, 2}) # type: ignore [arg-type] - - with pytest.raises(SimulationError): - Wait(timeout="1") # type: ignore [arg-type] - - with pytest.raises(SimulationError): - Wait(timeout=[1]) # type: ignore [arg-type] - - with pytest.raises(SimulationError): - Wait(timeout=[1, 2, 3]) # type: ignore [arg-type] - - -def test_base_request_event() -> None: - init_time = 1.23 - with EnvironmentContext(initial_time=init_time) as env: - base = BaseRequestEvent() - assert base.created_at == init_time, "Problem in environment time being stored in event" - assert base.env is env, "Problem in environment being stored in event" - - base.cancel() - - -def test_put_event_with_stores() -> None: - with EnvironmentContext() as env: - store = SIM.Store(env, capacity=1) - put_object = ("A Test Object", 1.0) - put_event = Put(store, put_object) - - assert put_event.calculate_time_to_complete() == 0.0, "Incorrect time to complete" - returned_object = put_event.as_event() - assert issubclass(returned_object.__class__, base.Put), ( - "Event returned is not simpy put event" - ) - env.run() - assert isinstance(returned_object, StorePut) - assert returned_object.item is put_object, "Wrong object put" - assert put_object in store.items - - put_object = ("A Second Test Object", 2.0) - put_event = Put(store, put_object) - event = put_event.as_event() - env.run() - assert not event.triggered, "Event shouldn't have completed" - assert event in store.put_queue, "Event is not waiting" - put_event.cancel() - env.run() - assert not event.triggered, "Event shouldn't have completed" - assert event not in store.put_queue, "Event is still in the store's queue" - - -def test_put_event_with_containers() -> None: - with EnvironmentContext() as env: - container = SIM.Container(env, capacity=1) - put_arg = 1.0 - put_event = Put(container, put_arg) - - assert put_event.calculate_time_to_complete() == 0.0, "Incorrect time to complete" - returned_object = put_event.as_event() - assert issubclass(returned_object.__class__, base.Put), ( - "Event returned is not simpy put event" - ) - env.run() - assert isinstance(returned_object, ContainerPut) - assert returned_object.amount == put_arg, "Wrong amount put" - assert container.level == put_arg - - put_arg = 2 - put_event = Put(container, put_arg) - event = put_event.as_event() - env.run() - assert not event.triggered, "Event shouldn't have completed" - assert event in container.put_queue, "Event is not waiting" - put_event.cancel() - env.run() - assert not event.triggered, "Event shouldn't have completed" - assert event not in container.put_queue, "Event is still in the store's queue" - - -def test_get_event_with_stores() -> None: - with EnvironmentContext() as env: - store = SIM.Store(env, capacity=1) - put_object = ("A Test Object", 1.0) - store.put(put_object) - env.run() - - event = Get(store) - assert event.calculate_time_to_complete() == 0.0, "Incorrect time to complete" - returned_object = event.as_event() - assert issubclass(returned_object.__class__, base.Get), ( - "Event returned is not simpy put event" - ) - - env.run() - assert isinstance(event._request_event, StoreGet) - item = event._request_event.value - assert item is put_object, "Returned item is not the original item" - item2 = event.get_value() - assert item is item2, "Same object from both methods" - - event = Get(store) - returned_object = event.as_event() - env.run() - assert returned_object in store.get_queue, "Event not in queue" - event.cancel() - assert returned_object not in store.get_queue, "Event is still in queue" - - -def test_get_event_with_containers() -> None: - with EnvironmentContext() as env: - container = SIM.Container(env, capacity=1) - put_arg = 1.0 - container.put(put_arg) - env.run() - - get_arg = 1.0 - event = Get(container, get_arg) - assert event.calculate_time_to_complete() == 0.0, "Incorrect time to complete" - returned_object = event.as_event() - assert issubclass(returned_object.__class__, base.Get), ( - "Event returned is not simpy put event" - ) - - env.run() - assert isinstance(event._request_event, ContainerGet) - amount = event._request_event.amount - assert amount == get_arg, "Returned item is not the original item" - with pytest.raises( - SimulationError, - match="'get_value' is not supported for Containers. " - "Check is_complete and use the amount you " - "requested", - ): - event.get_value() - - event = Get(container, get_arg) - returned_object = event.as_event() - env.run() - assert returned_object in container.get_queue, "Event not in queue" - event.cancel() - assert returned_object not in container.get_queue, "Event is still in queue" - - -def test_resource_events() -> None: - with EnvironmentContext() as env: - a_resource = SIM.Resource(env, capacity=1) - - request_object = ResourceHold(a_resource) - assert request_object._stage == "request", "Request object in wrong state" - request_object.as_event() - env.run() - - assert request_object._stage == "release", "Request object in wrong state" - assert a_resource.users[0] is request_object._request, "The user is the request object" - - new_request = ResourceHold(a_resource) - assert new_request._stage == "request", "Request object in wrong state" - new_request.as_event() - env.run() - - assert new_request._stage == "release", "Request object in wrong state" - assert new_request._request is not None - assert not new_request._request.processed, "Request went through when it shouldn't" - - # put the old one back - request_object.as_event() - env.run() - assert new_request._request is not None - assert new_request._request.processed, "Follow-on request didn't go through" - - newest_request = ResourceHold(a_resource) - assert newest_request._stage == "request", "Request object in wrong state" - newest_request.as_event() - env.run() - - # cancel it - assert newest_request._stage == "release", "Request object in wrong state" - assert newest_request._request is not None - assert not newest_request._request.processed, "Request went through when it shouldn't" - - assert newest_request._request in a_resource.put_queue, ( - "Resource isn't waiting to be gathered" - ) - with pytest.raises(SimulationError, match="Resource release requested.*?"): - newest_request.as_event() - - newest_request.cancel() - env.run() - assert newest_request._request not in a_resource.put_queue, ( - "Resource hasn't left the wait queue" - ) - - -def test_multi_event() -> None: - with EnvironmentContext() as env: - event1 = Wait(1.0) - event2 = Wait(1.5) - event = All(event1, event2) - assert event.calculate_time_to_complete() == 1.5 - - with EnvironmentContext() as env: - with pytest.warns(UserWarning): - event1 = Wait(1.0) - event3 = SIM.Timeout(env, 1.5) - All(event1, event3) # type: ignore [arg-type] - - with EnvironmentContext() as env: - w = Wait(1.0) - e = Event() - evt = All(w, e) - w.as_event() - e.as_event() - evt.as_event() - - with pytest.raises(SimulationError, match="failed to cancel"): - e.cancel() - evt.cancel() - - -def test_and_event() -> None: - with EnvironmentContext() as env: - - def run(env: SIM.Environment, data: dict[str, float]) -> SIMPY_GEN: - event1 = Wait(1.0) - event2 = Wait(1.5) - - event = All(event1, event2) - yield event.as_event() - data["time"] = env.now - - data: dict[str, float] = {} - env.process(run(env, data)) - env.run() - assert data["time"] == 1.5 - - -def test_or_event() -> None: - with EnvironmentContext() as env: - data = { - "time": 0.0, - } - - def run(env: SIM.Environment) -> SIMPY_GEN: - event1 = Wait(1.0) - event2 = Wait(1.5) - - event = Any(event1, event2) - yield event.as_event() - data["time"] = env.now - - env.process(run(env)) - env.run() - # SimPy still runs the simulation long enough to finish the timeout - assert data["time"] == 1.0 - - -def test_composite() -> None: - with EnvironmentContext() as env: - data = { - "time": 0.0, - "result": 0, - } - - def run(env: SIM.Environment) -> SIMPY_GEN: - event1 = Wait(1.0) - event2 = Wait(1.5) - - event3 = Wait(2.1) - event4 = Wait(0.9) - - event_a = Any(event1, event2) - event_b = All(event3, event4, event_a) - result = yield event_b.as_event() - data["time"] = env.now - data["result"] = len(result.events) - - env.process(run(env)) - env.run() - assert data["time"] == 2.1 - assert data["result"] == 4 - - -def test_process_in_multi() -> None: - with EnvironmentContext() as env: - - def a_process() -> SIMPY_GEN: - yield env.timeout(2) - - class Thing(Actor): - result = State[dict]() - events = State[list]() - - class TheTask(Task): - def task(self, *, actor: Thing) -> TASK_GEN: - wait = Wait(3.0) - proc = env.process(a_process()) - res = yield Any(wait, proc) - actor.events = [wait, proc] - actor.result = res - - t = Thing(name="Thing", result=None, events=None) - task = TheTask() - task.run(actor=t) - with pytest.warns(UserWarning): - env.run() - assert t.events[-1] in t.result - assert t.events[0] not in t.result - - -def test_rehearse_process_in_multi() -> None: - with EnvironmentContext() as env: - - def a_process() -> SIMPY_GEN: - yield env.timeout(2) - - class Thing(Actor): ... - - class TheTask(Task): - def task(self, *, actor: Thing) -> TASK_GEN: - wait = Wait(3.0) - proc = env.process(a_process()) - yield Any(wait, proc) - - t = Thing(name="Thing") - task = TheTask() - with pytest.raises(SimulationError, match="All events in a MultiEvent"): - with pytest.warns(UserWarning): - task.rehearse(actor=t) - - -def run_one(env: SIM.Environment, event: Event) -> SIMPY_GEN: - yield env.timeout(1.0) - event.succeed(data="here") - - -def run_two(env: SIM.Environment, event: Event, data: dict[str, float]) -> SIMPY_GEN: - yield event._event - data["time_two"] = env.now - - -def run_three(env: SIM.Environment, event: Event, data: dict[str, float]) -> SIMPY_GEN: - yield event._event - data["time_three"] = env.now - - -def run_four(env: SIM.Environment, event: Event) -> SIMPY_GEN: - yield env.timeout(1.1) - event.succeed() - - -def run_four_alt(env: SIM.Environment, event: Event) -> SIMPY_GEN: - yield env.timeout(1.1) - event.succeed() - - -def run_five(env: SIM.Environment, event: Event, data: dict[str, float]) -> SIMPY_GEN: - # Timeout until after the event suceeded, but before its reset - yield env.timeout(1.05) - yield event.as_event() - data["time_five"] = env.now - - -def test_basic_usage() -> None: - with EnvironmentContext() as env: - event = Event() - assert event._event is not None - assert isinstance(event._event, SIM.Event) - assert event._event is event.as_event() - assert event.is_complete() is False - - env.process(run_one(env, event)) - data: dict[str, float] = {} - env.process(run_two(env, event, data)) - env.run() - assert data["time_two"] == 1.0 - assert event.is_complete() - payload = event.get_payload() - assert payload == {"data": "here"} - - with pytest.raises(SimulationError): - event.succeed() - - assert event.calculate_time_to_complete() == 0.0 - - last_event = event._event - event.reset() - assert last_event is not event._event - - -def test_conflicts() -> None: - data: dict[str, float] = {} - with EnvironmentContext() as env: - event = Event() - env.process(run_one(env, event)) - env.process(run_two(env, event, data)) - env.process(run_three(env, event, data)) - env.run() - assert data["time_two"] == data["time_three"] - - data: dict[str, float] = {} - with EnvironmentContext() as env: - with pytest.raises(SimulationError): - event = Event() - env.process(run_one(env, event)) - env.process(run_two(env, event, data)) - env.process(run_three(env, event, data)) - env.process(run_four(env, event)) - env.run() - - data: dict[str, float] = {} - with EnvironmentContext() as env: - event = Event() - env.process(run_one(env, event)) - env.process(run_two(env, event, data)) - env.process(run_three(env, event, data)) - env.process(run_four_alt(env, event)) - env.process(run_five(env, event, data)) - env.run() - assert data["time_two"] == data["time_three"] - assert data["time_five"] == 1.1 - - -def test_resubmit_wait_events() -> None: - # Test the bug found in github issue 41 - # Where Wait wasn't acting like timeout for being resubmitted. - - # This illustrates the simpy behavior - with EnvironmentContext() as env: - w1 = Wait(1.1) - w2 = Wait(2.2) - - # put the events into simpy - w1.as_event() - w2.as_event() - - env.run() - assert env.now == 2.2 - - # even when the timeout has no callbacks, it completes - with EnvironmentContext() as env: - w1 = Wait(1.1) - w2 = Wait(2.2) - - def _proc() -> SIMPY_GEN: - yield w1.as_event() | w2.as_event() - assert env.now == 1.1 - - env.process(_proc()) - env.run() - assert env.now == 2.2 - - # if we re-wait on w2, it should end at the right time. - with EnvironmentContext() as env: - w1 = Wait(1.1) - w2 = Wait(2.2) - - def _proc() -> SIMPY_GEN: - yield w1.as_event() | w2.as_event() - assert env.now == 1.1 - yield w2.as_event() - assert env.now == 2.2 - - env.process(_proc()) - env.run() - assert env.now == 2.2 - - -def test_resubmit_get_put_events() -> None: - # Make sure that get/put events don't hang. - with EnvironmentContext() as env: - store1 = SIM.Store(env) - store2 = SIM.Store(env) - - def _put_stuff() -> SIMPY_GEN: - yield Wait(1.0).as_event() - yield store1.put("thing") - yield Wait(1.0).as_event() - yield store2.put("other") - - def _get_stuff() -> SIMPY_GEN: - g1 = Get(store1) - g2 = Get(store2) - yield g1.as_event() | g2.as_event() - assert g1.is_complete() - assert g1.get_value() == "thing" - assert env.now == 1.0 - yield g2.as_event() - assert env.now == 2.0 - assert g2.is_complete() - assert g2.get_value() == "other" - - env.process(_put_stuff()) - env.process(_get_stuff()) - env.run() - - # run the same through UPSTAGE - with EnvironmentContext() as env: - store1 = SIM.Store(env) - store2 = SIM.Store(env) - - def _put_stuff() -> SIMPY_GEN: - yield Wait(1.0).as_event() - yield store1.put("thing") - yield Wait(1.0).as_event() - yield store2.put("other") - - def _get_stuff() -> SIMPY_GEN: - g1 = Get(store1) - g2 = Get(store2) - yield Any(g1, g2).as_event() - assert g1.is_complete() - assert g1.get_value() == "thing" - assert env.now == 1.0 - yield g2.as_event() - assert env.now == 2.0 - assert g2.is_complete() - assert g2.get_value() == "other" - - env.process(_put_stuff()) - env.process(_get_stuff()) - env.run() - - -if __name__ == "__main__": - test_multi_event() diff --git a/src/upstage_des/test/test_geography/__init__.py b/src/upstage_des/test/test_geography/__init__.py deleted file mode 100644 index a73c948..0000000 --- a/src/upstage_des/test/test_geography/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. diff --git a/src/upstage_des/test/test_geography/conftest.py b/src/upstage_des/test/test_geography/conftest.py deleted file mode 100644 index 8a28909..0000000 --- a/src/upstage_des/test/test_geography/conftest.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import random - -import pytest - - -@pytest.fixture() -def atl() -> tuple[float, float]: - return (33.7490, -84.3880) - - -@pytest.fixture() -def nas() -> tuple[float, float]: - return (36.1627, -86.7816) - - -@pytest.fixture() -def nyc() -> tuple[float, float]: - return (40.7128, -74.0060) - - -@pytest.fixture() -def lax() -> tuple[float, float]: - return (34.0522, -118.2437) - - -@pytest.fixture() -def tall() -> tuple[float, float]: - return (30.4383, -84.2807) - - -def randvals(rows: int, cols: int) -> list[list[float]]: - ans: list[list[float]] = [] - for _ in range(rows): - v = [random.random() for _ in range(cols)] - ans.append(v) - return ans - - -@pytest.fixture() -def random_lla() -> list[tuple[float, float, float]]: - lla = randvals(10, 3) - lat, lon, alt = zip(*lla) - lat = [-90 + 180 * la for la in lat] - lon = [-180 + 360 * lo for lo in lon] - alt = [a * 10_000 for a in alt] - return [(a, b, c) for a, b, c in zip(lat, lon, alt)] - - -POS = tuple[float, float, float] - - -@pytest.fixture( - params=[ - (0, 0, 0, 100, "nmi", ["ENTER", "EXIT"]), - (5000, 5000, 0, 100, "nmi", ["ENTER", "EXIT"]), - (0, 0, 0, 190, "nmi", ["START_INSIDE", "EXIT"]), - (5000, 5000, 0, 190, "nmi", ["START_INSIDE", "EXIT"]), - (0, 0, 0, 220, "nmi", ["START_INSIDE", "END_INSIDE"]), - (5000, 5000, 0, 220, "nmi", ["START_INSIDE", "END_INSIDE"]), - ], -) -def intersect_positions( - nas: tuple[float, float], - tall: tuple[float, float], - atl: tuple[float, float], - request: pytest.FixtureRequest, -) -> tuple[tuple[POS, POS, POS], float, str, list[str]]: - # degrees and meters - parm: tuple[float, float, float, float, str, list[str]] = request.param - start_alt, finish_alt, sensor_alt, sensor_range, range_units, answer = parm - - start_lla = (nas[0], nas[1], start_alt) - finish_lla = (tall[0], tall[1], finish_alt) - sensor_lla = (atl[0], atl[1], sensor_alt) - return (start_lla, finish_lla, sensor_lla), sensor_range, range_units, answer - - -@pytest.fixture -def short_intersections() -> tuple[tuple[POS, POS, POS], float, str]: - start_lla = (61.10051577739581, -154.64858364056806, 100.0) - finish_lla = (61.15466234550906, -154.77763243723408, 100.0) - sensor_lla = (58.67779481, -154.11651336, 0.0) - sensor_radius = 277799.8988808368 - return (start_lla, finish_lla, sensor_lla), sensor_radius, "m" diff --git a/src/upstage_des/test/test_geography/test_conversions.py b/src/upstage_des/test/test_geography/test_conversions.py deleted file mode 100644 index 74b012a..0000000 --- a/src/upstage_des/test/test_geography/test_conversions.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.geography import conversions, spherical, wgs84 -from upstage_des.geography.conversions import BaseConversions - -SC = conversions.SphericalConversions -WSGC = conversions.WGS84Conversions -SC2 = spherical.Spherical -WSGC2 = wgs84.WGS84 - - -@pytest.mark.parametrize("use", [SC, SC2, WSGC, WSGC2]) -def test_conversions(use: BaseConversions, random_lla: list[tuple[float, float, float]]) -> None: - # Do a back and forth test of random Lat Lon Alt - ecef = use.lla2ecef(random_lla) - lla_from_ecef = use.ecef2lla(ecef) - for a, b in zip(lla_from_ecef, random_lla): - assert pytest.approx(a) == b diff --git a/src/upstage_des/test/test_geography/test_intersections.py b/src/upstage_des/test/test_geography/test_intersections.py deleted file mode 100644 index d703b50..0000000 --- a/src/upstage_des/test/test_geography/test_intersections.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.geography import WGS84, Spherical, get_intersection_locations -from upstage_des.motion.cartesian_model import ray_intersection - -from .conftest import POS - - -@pytest.mark.parametrize("earth", [WGS84, Spherical]) -def test_intersections( - intersect_positions: tuple[tuple[POS, POS, POS], float, str, list[str]], - earth: WGS84 | Spherical, -) -> None: - pos, sensor_range, range_units, answer = intersect_positions - start_lla, finish_lla, sensor_lla = pos - sensor_loc = (sensor_lla[0], sensor_lla[1]) - intersects = get_intersection_locations( - start_lla, - finish_lla, - sensor_lla, - sensor_range, - range_units, - earth, - dist_between=9260, - subdivide_levels=[20, 20], - ) - # without visibility, the intersections should be very close to the right - # range, unless they are inside the range due to START_IN - for i, an in zip(intersects, answer): - assert i.kind == an - loc = (i.begin[0], i.begin[1]) - dist = earth.distance(loc, sensor_loc, range_units) - if an in ["EXIT", "ENTER"]: - assert sensor_range == pytest.approx(dist, rel=0.001) - else: - assert dist < sensor_range - - -@pytest.mark.parametrize("earth", [WGS84, Spherical]) -def test_short_intersections( - earth: WGS84 | Spherical, short_intersections: tuple[tuple[POS, POS, POS], float, str] -) -> None: - pos, sensor_range, range_units = short_intersections - start_lla, finish_lla, sensor_lla = pos - _ = get_intersection_locations( - start_lla, - finish_lla, - sensor_lla, - sensor_range, - range_units, - earth, - dist_between=9260, - subdivide_levels=[20, 20], - ) - - -def test_ray_trace() -> None: - input_1 = ((0, 2), (0, 1.8), (0, 0), (1, 1)) - input_2 = ((0, 2), (1, 2), (0, 0), (1, 1)) - input_3 = ((0, 2, 0), (0, 1.8, 0), (0, 0, 0), (1, 1, 1)) - input_4 = ((0, 2, 0), (1, 2, 0), (0, 0, 0), (1, 1, 1)) - - points1, _ = ray_intersection(*input_1, 1.0) - for x, y in points1: - assert pytest.approx(0) == x - assert pytest.approx(1) == abs(y) - - points2, _ = ray_intersection(*input_2, 1.0) - assert not points2 - - points3, _ = ray_intersection(*input_3, 1.0) - for x, y, z in points3: - assert pytest.approx(0) == x - assert pytest.approx(1) == abs(y) - - points4, _ = ray_intersection(*input_4, 1.0) - assert not points4 diff --git a/src/upstage_des/test/test_geography/test_spherical.py b/src/upstage_des/test/test_geography/test_spherical.py deleted file mode 100644 index 4c285d2..0000000 --- a/src/upstage_des/test/test_geography/test_spherical.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.geography import Spherical - - -def test_distance(atl: tuple[float, float], nas: tuple[float, float]) -> None: - dist = Spherical.distance(atl, nas) - assert dist == pytest.approx(186.94317974521996) - - -def test_bearing(atl: tuple[float, float], nas: tuple[float, float]) -> None: - bearing = Spherical.bearing(atl, nas) - assert bearing == pytest.approx(321.5766379719283) - - -def test_linspace(atl: tuple[float, float], nas: tuple[float, float]) -> None: - latlons = Spherical.geo_linspace(atl, nas, 10) - assert len(latlons) == 11 - lats, lons = zip(*latlons) - assert pytest.approx(lats[0]) == atl[0] - assert pytest.approx(lats[-1]) == nas[0] - assert pytest.approx(lons[0]) == atl[1] - assert pytest.approx(lons[-1]) == nas[1] - - -def test_geo_circle(atl: tuple[float, float]) -> None: - latlons = Spherical.geo_circle(atl, radius=100, num_points=20) - for lat, lon in latlons: - d = Spherical.distance(atl, (lat, lon)) - assert d == pytest.approx(100) - - -def test_point_along(atl: tuple[float, float], nas: tuple[float, float]) -> None: - pt = Spherical.point_along(atl, nas, 0.0) - assert pt[0] == atl[0] - assert pt[1] == atl[1] - - -def test_bearing_dist_from_point(atl: tuple[float, float], nas: tuple[float, float]) -> None: - bearing = Spherical.bearing(atl, nas) - dist = Spherical.distance(atl, nas) - pt = Spherical.point_from_bearing_dist(atl, bearing, dist, "nmi") - new_dist = Spherical.distance(pt, nas, "nmi") - assert new_dist <= 1e-4 - - -def test_cross_track( - nyc: tuple[float, float], lax: tuple[float, float], atl: tuple[float, float] -) -> None: - res = Spherical.cross_track_point(nyc, lax, atl) - dist = Spherical.cross_track_distance(nyc, lax, atl) - dist2 = Spherical.distance(res, atl) - assert pytest.approx(dist) == dist2 diff --git a/src/upstage_des/test/test_geography/test_wsg84.py b/src/upstage_des/test/test_geography/test_wsg84.py deleted file mode 100644 index ebf29ae..0000000 --- a/src/upstage_des/test/test_geography/test_wsg84.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.geography import WGS84 - - -def test_distance_and_bearing(atl: tuple[float, float], nas: tuple[float, float]) -> None: - dist = WGS84.distance(atl, nas, "nmi") - assert dist == pytest.approx(186.64143600171283) - bearing = WGS84.bearing(atl, nas) - assert bearing == pytest.approx(321.4499587022779) - d2, b2 = WGS84.distance_and_bearing(atl, atl) - assert pytest.approx(0) == d2 - - -def test_linspace(atl: tuple[float, float], nas: tuple[float, float]) -> None: - latlons = WGS84.geo_linspace(atl, nas, 10) - assert len(latlons) == 11 - lats, lons = zip(*latlons) - assert lats[0] == atl[0] - assert lats[-1] == pytest.approx(nas[0]) - assert lons[0] == pytest.approx(atl[1]) - assert lons[-1] == pytest.approx(nas[1]) - - -def test_geo_circle(atl: tuple[float, float]) -> None: - latlons = WGS84.geo_circle(atl, radius=100, num_points=20) - for lat, lon in latlons: - d = WGS84.distance(atl, (lat, lon)) - assert d == pytest.approx(100, rel=0.001) - - -def test_bearing_dist_from_point(atl: tuple[float, float], nas: tuple[float, float]) -> None: - dist, bearing = WGS84.distance_and_bearing(atl, nas, units="km") - pt = WGS84.point_from_bearing_dist(atl, bearing, dist, distance_units="km") - new_dist = WGS84.distance(pt, nas, units="km") - assert 0 == pytest.approx(new_dist, abs=0.01) - - -def test_addtional_2() -> None: - point_1 = (50 + 3 / 60 + 58.76 / 3600, -(5 + 42 / 60 + 53.10 / 3600)) - point_2 = (58 + 38 / 60 + 38.48 / 3600, -(3 + 4 / 60 + 12.34 / 3600)) - dist, bearing = WGS84.distance_and_bearing(point_1, point_2, units="km") - assert dist == pytest.approx(969.954114) - actual_bearing = 9 + 8 / 60 + 30.70 / 3600 - assert bearing == pytest.approx(actual_bearing) - - -def test_addtional_3() -> None: - point_1 = (-(37 + 57 / 60 + 3.72030 / 3600), 144 + 25 / 60 + 29.52440 / 3600) - point_2 = (-(37 + 39 / 60 + 10.15610 / 3600), 143 + 55 / 60 + 35.38390 / 3600) - dist, bearing = WGS84.distance_and_bearing(point_1, point_2, units="km") - assert dist == pytest.approx(54.972271) - actual_bearing = 306 + 52 / 60 + 5.37 / 3600 - assert bearing == pytest.approx(actual_bearing) diff --git a/src/upstage_des/test/test_great_circle_calcs.py b/src/upstage_des/test/test_great_circle_calcs.py deleted file mode 100644 index 3ad769f..0000000 --- a/src/upstage_des/test/test_great_circle_calcs.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from math import degrees, pi, radians - -import pytest - -import upstage_des.api as UP -from upstage_des.motion.great_circle_calcs import ( - get_course_rad, - get_dist_rad, - get_great_circle_points, - get_pos_from_points_and_distance, -) - - -def test_distance() -> None: - """Test great circle distance calc.""" - with UP.EnvironmentContext(): - p1 = UP.GeodeticLocation(0, 180, 0).to_radians() - p2 = UP.GeodeticLocation(1, 180, 0).to_radians() - assert pytest.approx(get_dist_rad(p1, p2)) == pi / 180 - - p3 = UP.GeodeticLocation(0, 180, 0).to_radians() - p4 = UP.GeodeticLocation(0, 181, 0).to_radians() - assert pytest.approx(get_dist_rad(p3, p4)) == pi / 180 - - -def test_course() -> None: - """Test great circle course calc.""" - with UP.EnvironmentContext(): - p1 = UP.GeodeticLocation(0, 180, 0).to_radians() - p2 = UP.GeodeticLocation(1, 180, 0).to_radians() - assert pytest.approx(get_course_rad(p1, p2)) == 2 * pi - - p3 = UP.GeodeticLocation(0, 180, 0).to_radians() - p4 = UP.GeodeticLocation(0, 181, 0).to_radians() - assert pytest.approx(get_course_rad(p3, p4)) == 3 * pi / 2.0 - - -def test_position_from_point_distance() -> None: - """Test position from point and distance calc.""" - with UP.EnvironmentContext(): - p1 = UP.GeodeticLocation(0, 180, 0).to_radians() - p2 = UP.GeodeticLocation(1, 180, 0).to_radians() - dist = 0.5 * (pi / 180) - half_point = get_pos_from_points_and_distance(p1, p2, dist) - assert pytest.approx(degrees(half_point[0])) == 0.5 - assert pytest.approx(degrees(half_point[1])) == -180 - - -def test_great_circle_points() -> None: - """Test calculation of points on creat circle path.""" - with UP.EnvironmentContext(): - p1 = UP.GeodeticLocation(0, 180, 0).to_radians() - p2 = UP.GeodeticLocation(5, 180, 0).to_radians() - p3 = UP.GeodeticLocation(3, 180, 0).to_radians() - dist = pi / 180 - x = get_great_circle_points(p1, p2, p3, dist) - assert x is not None - points, distances = x - assert len(points) == 2 - - assert pytest.approx(points[0][0]) == radians(2) - assert pytest.approx(points[0][1]) == radians(-180) - assert pytest.approx(points[1][0]) == radians(4) - assert pytest.approx(points[1][1]) == radians(-180) - assert pytest.approx(distances[0]) == radians(2) - assert pytest.approx(distances[1]) == radians(4) - - -def test_caching() -> None: - with UP.EnvironmentContext(): - get_dist_rad.cache_clear() - get_course_rad.cache_clear() - get_pos_from_points_and_distance.cache_clear() - get_great_circle_points.cache_clear() - - p1 = UP.GeodeticLocation(0, 180, 0).to_radians() - p2 = UP.GeodeticLocation(1, 180, 0).to_radians() - p3 = UP.GeodeticLocation(3, 180, 0).to_radians() - dist = pi / 180 - - get_dist_rad(p1, p2) - get_dist_rad(p1, p2) - cache_info = get_dist_rad.cache_info() - assert cache_info.hits == 1 - assert cache_info.currsize == 1 - - get_course_rad(p1, p2) - get_course_rad(p1, p2) - cache_info = get_course_rad.cache_info() - assert cache_info.hits == 1 - assert cache_info.currsize == 1 - - get_pos_from_points_and_distance(p1, p2, dist) - get_pos_from_points_and_distance(p1, p2, dist) - cache_info = get_pos_from_points_and_distance.cache_info() - assert cache_info.hits == 1 - assert cache_info.currsize == 1 - - get_great_circle_points(p1, p2, p3, dist) - get_great_circle_points(p1, p2, p3, dist) - cache_info = get_great_circle_points.cache_info() - assert cache_info.hits == 1 - assert cache_info.currsize == 1 diff --git a/src/upstage_des/test/test_integration.py b/src/upstage_des/test/test_integration.py deleted file mode 100644 index f9b1e63..0000000 --- a/src/upstage_des/test/test_integration.py +++ /dev/null @@ -1,313 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import simpy as SIM -from simpy import Environment, Process - -from upstage_des.actor import Actor -from upstage_des.base import EnvironmentContext, MockEnvironment -from upstage_des.constants import PLANNING_FACTOR_OBJECT -from upstage_des.events import Any, Get, Put, ResourceHold, Wait -from upstage_des.states import LinearChangingState, State -from upstage_des.task import Task -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class ActorForTest(Actor): - dummy = State[list]() - - -class StateActor(Actor): - fuel = LinearChangingState(recording=True) - fuel_burn = State[float](recording=True) - - -class Flight: - def __init__(self, code: int) -> None: - self.code = code - self.dummy: list = [] - - -class AirplaneTask(Task): - limit: float - rate: float - store: SIM.Store - orders_time: float - reason: list - - def task(self, *, actor: StateActor) -> TASK_GEN: - # this task could be thought of as loitering, waiting for a get - # request, interrupt, or prescribed failure - time_to_leave = (actor.fuel - self.limit) / self.rate - - leave_event = Wait(time_to_leave) - orders_event = Get(self.store, rehearsal_time_to_complete=self.orders_time) - - actor.activate_linear_state(state="fuel", task=self, rate=-self.rate) - any_event = Any(leave_event, orders_event) - yield any_event - actor.deactivate_state(state="fuel", task=self) - - if orders_event.is_complete(): - self.reason.append(orders_event.get_value()) - else: - orders_event.cancel() - if leave_event.is_complete(): - self.reason.append("LEAVE") - - -class BigTask(Task): - time: float - maintenance_bay: SIM.Resource - broken_vehicle_depot: SIM.Store - fixed_vehicle_depot: SIM.Store - - def task( - self, - *, - actor: Flight, - ) -> TASK_GEN: - # a function to mimic extra code needed to perform a task - # TODO: This should be wrapped to handle the planning answer - def test_flight(flight: Flight, planning_answer: float = 3.0) -> float: - if flight is PLANNING_FACTOR_OBJECT: - return planning_answer - return flight.code * 1.5 - - yield Wait(self.time) - actor.dummy.append(self.time) - - # wait for a resource, then do stuff, then give it back - # get a bay for the actor to do work in - resource_event = ResourceHold(self.maintenance_bay) - yield resource_event - - # get a vehicle to fix - vehicle_to_fix = yield Get(self.broken_vehicle_depot) - - # fix it - time_to_fix = test_flight(vehicle_to_fix, planning_answer=6.0) - yield Wait(time_to_fix) - actor.dummy.append(time_to_fix) - - # put it in fixed - yield Put(self.fixed_vehicle_depot, vehicle_to_fix) - - # leave the job site - yield resource_event - - -def interrupting_task(*, env: Environment, time: float, other_task: Process) -> SIMPY_GEN: - yield env.timeout(time) - if other_task.is_alive: - other_task.interrupt("cancelling") - - -def test_event_store_returns() -> None: - with EnvironmentContext() as env: - store = SIM.Store(env, capacity=1) - put_object = ("A Test Object", 1.0) - store.put(put_object) - env.run() - - class DoARun(Task): - def task(self, *, actor: ActorForTest) -> TASK_GEN: - return_value = yield Get(store) - actor.dummy.append(return_value) - - actor = ActorForTest(name="testing", dummy=[]) - actor_2 = DoARun().rehearse( - actor=actor, - ) - - assert actor_2.dummy[0] is PLANNING_FACTOR_OBJECT - - _ = DoARun().run( - actor=actor, - ) - env.run() - assert actor.dummy[0] is put_object, "Object returned is not the correct object" - - -def test_task_with_all_events() -> None: - with EnvironmentContext() as env: - store = SIM.Store(env, capacity=2) - store2 = SIM.Store(env, capacity=2) - resource = SIM.Resource(env) - - f = Flight(2) - f2 = Flight(3) - store.put(f) - store.put(f2) - env.run() - - actor = ActorForTest(name="maintenance person", dummy=[]) - - # test the task - bt = BigTask() - bt.time = 2.0 - bt.broken_vehicle_depot = store - bt.maintenance_bay = resource - bt.fixed_vehicle_depot = store2 - test_actor = bt.rehearse( - actor=actor, - ) - - # check that the returned test actor has the expected entries in its state - assert test_actor.dummy[0] == 2.0, "Wrong result in test actor" - assert test_actor.dummy[1] == 6.0, "Wrong result in test actor for fake store object" - assert f in store.items, "Item was removed when it shouldn't have been" - assert f2 in store.items, "Item was removed when it shouldn't have been" - # run the process for real - bt = BigTask() - bt.time = 2.0 - bt.broken_vehicle_depot = store - bt.maintenance_bay = resource - bt.fixed_vehicle_depot = store2 - _ = bt.run( - actor=actor, - ) - - env.run() - assert actor.dummy[0] == 2.0, "Wrong result in test actor" - assert actor.dummy[1] == f.code * 1.5, "Wrong result in test actor for fake store object" - assert f not in store.items, "Item wasn't removed when it should have been" - assert f2 in store.items, "Item was removed when it shouldn't have been" - assert f in store2.items, "Item wasn't moved to the next store" - - -def test_task_with_get() -> None: - with EnvironmentContext() as env: - orders = SIM.Store(env) - actor = StateActor(name="Airplane", fuel=100, fuel_burn=5.2) - result = [] - order_time = 12.3 - - class SimpleTask(Task): - def task(self, *, actor: StateActor) -> TASK_GEN: - event = Get(orders, rehearsal_time_to_complete=order_time) - res = yield event - result.append(res) - result.append(self.env.now) - result.append(event) - result.append(self.env) - - SimpleTask().rehearse( - actor=actor, - ) - - assert isinstance(result[3], MockEnvironment), f"Not a mock environment: {result[3]}" - assert result[0] is PLANNING_FACTOR_OBJECT - assert result[1] == 12.3 - assert result[2].is_complete, "Tested event believes it completed" - - -def test_task_rehearsal_with_cancels() -> None: - with EnvironmentContext() as env: - orders = SIM.Store(env) - actor = StateActor(name="Airplane", fuel=100, fuel_burn=5.2) - - at = AirplaneTask() - at.rate = 1.2 - at.limit = 5 - at.store = orders - at.orders_time = 0.0 - at.reason = [] - - tested_actor = at.rehearse(actor=actor) - - assert at.reason[0] == PLANNING_FACTOR_OBJECT - assert len(at.reason) == 1, "Wrong number of values returned" - assert tested_actor.fuel == 100 - - at = AirplaneTask() - at.rate = 1.2 - at.limit = 5 - at.store = orders - at.orders_time = 90.0 - at.reason = [] - tested_actor = at.rehearse( - actor=actor, - ) - - assert at.reason[0] == "LEAVE" - assert len(at.reason) == 1, "Wrong number of values returned" - assert tested_actor.fuel == 5 - - -def test_task_with_cancels() -> None: - with EnvironmentContext() as env: - orders = SIM.Store(env) - actor = StateActor(name="Airplane", fuel=100, fuel_burn=5.2) - reason: list[str] = [] - - at = AirplaneTask() - at.rate = 1.2 - at.limit = 5 - at.store = orders - at.orders_time = 0.0 - at.reason = reason - - at.run( - actor=actor, - ) - - # run the task - env.run() - assert reason[0] == "LEAVE", "Wrong leave reason" - assert env.now == (100 - 5) / 1.2, "Wrong environment time" - assert actor.fuel == 5, "Wrong fuel" - - # test the task when orders are given - def give_orders(env: SIM.Environment, time: float, orders: SIM.Store) -> SIMPY_GEN: - yield env.timeout(time) - yield orders.put("STOP WHAT YOU ARE DOING") - - ############## - with EnvironmentContext() as env: - orders = SIM.Store(env) - actor = StateActor(name="Airplane", fuel=100, fuel_burn=5.2) - - at = AirplaneTask() - at.rate = 1.2 - at.limit = 5 - at.store = orders - at.orders_time = 0.0 - at.reason = [] - - at.run( - actor=actor, - ) - _ = env.process(give_orders(env, 23.0, orders)) - env.run() - assert at.reason[0] == "STOP WHAT YOU ARE DOING" - - ############## - # test the task when orders are given, but it's interrupted beforehand - with EnvironmentContext() as env: - orders = SIM.Store(env) - actor = StateActor(name="Airplane", fuel=100, fuel_burn=5.2) - - at = AirplaneTask() - at.rate = 1.2 - at.limit = 5 - at.store = orders - at.orders_time = 0.0 - at.reason = [] - - task_proc = at.run( - actor=actor, - ) - - _ = env.process(give_orders(env, 23.0, orders)) - _ = env.process(interrupting_task(env=env, time=16.5, other_task=task_proc)) - env.run(until=60) - - # no reason should be given - assert not at.reason - expected_fuel = 100 - (16.5 * 1.2) - assert actor.fuel == expected_fuel - assert len(env._queue) == 1, "End environment queue is too long" - assert env._queue[0][3].callbacks == [], "Timeout had callbacks that should be cleared" diff --git a/src/upstage_des/test/test_knowledge.py b/src/upstage_des/test/test_knowledge.py deleted file mode 100644 index 8b4f72e..0000000 --- a/src/upstage_des/test/test_knowledge.py +++ /dev/null @@ -1,51 +0,0 @@ -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - - -class KnowEvenTask(UP.Task): - def task(self, *, actor: UP.Actor) -> TASK_GEN: - evt = actor.create_knowledge_event(name="EvtName") - actor.create_knowledge_event(name="other evt") - self.set_actor_knowledge(actor, "finished", False) - yield evt - self.set_actor_knowledge(actor, "finished", True, overwrite=True) - - def on_interrupt(self, *, actor: UP.Actor, cause: str) -> UP.InterruptStates: - self.set_actor_knowledge(actor, "cause", cause) - return UP.InterruptStates.END - - -def test_knowledge_event_clear() -> None: - with UP.EnvironmentContext() as env: - act = UP.Actor(name="Example") - - task = KnowEvenTask() - task.run(actor=act) - - env.run() - assert not act._knowledge["finished"] - act.succeed_knowledge_event(name="EvtName") - env.run() - assert act._knowledge["finished"] - assert "EvtName" not in act._knowledge - assert "other evt" in act._knowledge - - with UP.EnvironmentContext() as env: - act = UP.Actor(name="Example") - - task = KnowEvenTask() - proc = task.run(actor=act) - - env.run() - assert not act._knowledge["finished"] - proc.interrupt(cause="ending") - env.run() - assert "EvtName" not in act._knowledge - assert not act._knowledge["finished"] - assert act._knowledge["cause"] == "ending" - # Only the yielded event should be cleared. - assert "other evt" in act._knowledge - - -if __name__ == "__main__": - test_knowledge_event_clear() diff --git a/src/upstage_des/test/test_locations.py b/src/upstage_des/test/test_locations.py deleted file mode 100644 index cae5d6f..0000000 --- a/src/upstage_des/test/test_locations.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.actor import Actor -from upstage_des.api import EnvironmentContext, SimulationError, add_stage_variable -from upstage_des.data_types import GeodeticLocation -from upstage_des.geography import Spherical -from upstage_des.states import GeodeticLocationChangingState - -# example lat lon alts -ATLANTA = [33.7490, -84.3880, 1050] -DENVER = [39.7392, -104.9903, 30_000] -SAN_FRAN = [37.7749, -122.4194, 0] - - -def test_create_geodetic() -> None: - with EnvironmentContext(): - atl = GeodeticLocation( - *ATLANTA, - ) - san_fran = GeodeticLocation( - *SAN_FRAN, - ) - - atl[0] == ATLANTA[0] - with pytest.raises(IndexError): - san_fran[3] - - with pytest.raises(ValueError): - atl - 1 - - with pytest.raises(ValueError): - atl == 1 - - assert atl != san_fran - assert atl == atl - assert san_fran == san_fran.copy() - - -def test_subtraction_geodetic() -> None: - with EnvironmentContext(): - add_stage_variable("stage_model", Spherical) - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - - atl = GeodeticLocation( - ATLANTA[0], - ATLANTA[1], - 0.0, # set feet to zero to test the distance against no altitude change - ) - san_fran = GeodeticLocation( - *SAN_FRAN, - ) - dist = atl - san_fran - assert abs(dist - 1857.8524277061492) <= 1e-6 - - d = atl - atl.copy() - assert d == 0 - - atl = GeodeticLocation(*ATLANTA) - den = GeodeticLocation(*DENVER) - dist = atl.dist_with_altitude(den) - assert abs(dist - 1052.67744200) <= 1e-6 - - -def test_create_geodetic_changing() -> None: - class StateTest(Actor): - loc_state = GeodeticLocationChangingState( - recording=True, - ) - - with EnvironmentContext(): - add_stage_variable("stage_model", Spherical) - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - - tester = StateTest( - name="Geodetic Loc", - loc_state=GeodeticLocation(*ATLANTA), - ) - assert tester.loc_state == GeodeticLocation(*ATLANTA) - assert tester.loc_state != GeodeticLocation(*DENVER) - - -def test_active_geodetic_changing() -> None: - class StateTest(Actor): - loc_state = GeodeticLocationChangingState( - recording=True, - ) - - with EnvironmentContext(): - add_stage_variable("stage_model", Spherical) - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - - tester = StateTest( - name="Geodetic Loc", - loc_state=GeodeticLocation(*ATLANTA), - ) - waypoints = [ - GeodeticLocation(*DENVER), - GeodeticLocation(*SAN_FRAN), - ] - tester.activate_state( - state="loc_state", - task="dummy_task", # type: ignore [arg-type] - speed=1.0, - waypoints=waypoints, - ) - assert "loc_state" in tester._active_states - assert tester._active_states["loc_state"] == tester.get_active_state_data("loc_state") - - # check the data created - # ask for the state to update it - tester.loc_state - data = tester.get_active_state_data("loc_state") - assert "path_data" in data - assert len(data["path_data"]["times"]) == 2 - - assert tester.loc_state == GeodeticLocation(*ATLANTA) - tester.env.run(until=100) - assert tester.loc_state != GeodeticLocation(*ATLANTA) - assert tester.loc_state.alt > ATLANTA[2] - - # run time to reach Denver - tester.env.run(until=1052.6666594454714) - assert abs(tester.loc_state.lat - DENVER[0]) <= 1e-6 - assert abs(tester.loc_state.lon - DENVER[1]) <= 1e-6 - assert abs(tester.loc_state.alt - DENVER[2]) <= 1e-6 - - # make sure it moves to the next waypoint - tester.env.run(until=1200) - tester.loc_state - assert tester.loc_state.alt < DENVER[2] - assert tester.loc_state.lon < DENVER[1] - - tester.env.run(until=1052.6666594454714 + 824.0883665214037) - tester.loc_state - assert abs(tester.loc_state.lat - SAN_FRAN[0]) <= 1e-6 - assert abs(tester.loc_state.lon - SAN_FRAN[1]) <= 1e-6 - assert abs(tester.loc_state.alt - SAN_FRAN[2]) <= 1e-6 - - tester.env.run(until=tester.env.now + 10) - with pytest.raises(SimulationError): - tester.loc_state diff --git a/src/upstage_des/test/test_monitoring.py b/src/upstage_des/test/test_monitoring.py deleted file mode 100644 index 7c7987a..0000000 --- a/src/upstage_des/test/test_monitoring.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Tests for a bug where bad queue order keeps Monitoring*Stores from working.""" - -from simpy import Container, Environment, FilterStore, Store - -from upstage_des.base import EnvironmentContext -from upstage_des.events import Get, Put -from upstage_des.resources.monitoring import ( - SelfMonitoringContainer, - SelfMonitoringFilterStore, - SelfMonitoringSortedFilterStore, - SelfMonitoringStore, -) -from upstage_des.type_help import SIMPY_GEN - - -def _container_check(container: Container, env: Environment) -> dict[str, tuple[float, float]]: - """Run a procedure against a container.""" - data: dict[str, tuple[float, float]] = {} - - def _get_proc() -> SIMPY_GEN: - yield env.timeout(1.5) - yield Get(container, 1.0).as_event() - data["one"] = (env.now, 1.0) - - def _put_one() -> SIMPY_GEN: - yield env.timeout(1) - yield Put(container, 1.0).as_event() - data["put one"] = (env.now, 1.0) - - def _put_two() -> SIMPY_GEN: - yield env.timeout(1) - yield Put(container, 0.8).as_event() - data["put two"] = (env.now, 1.0) - - env.process(_get_proc()) - env.process(_put_one()) - env.process(_put_two()) - - env.run() - - return data - - -def test_monitoring_container_get() -> None: - # The container outputs "True" if a put is successful (to the trigger), - # so our original failure to output True should cause a successful - # follow-on put to not happen. - - with EnvironmentContext() as env: - smc = Container(env, init=1.1, capacity=2) - data = _container_check(smc, env) - - assert data.get("one", 0.0) == (1.5, 1.0) - assert data.get("put one", 0.0) == (1.5, 1.0) - assert data.get("put two", 0.0) == (1.5, 1.0) - - with EnvironmentContext() as env: - smc = SelfMonitoringContainer(env, init=1.1, capacity=2) - data2 = _container_check(smc, env) - - assert data2 == data - - -def _store_process( - store: Store, env: Environment, filter: bool = True -) -> dict[str, tuple[float, int] | list[int]]: - """Run a filtering store process.""" - store.items.append(2) - - data: dict[str, tuple[float, int] | list[int]] = {} - - def _proc_one() -> SIMPY_GEN: - if filter: - res = yield Get(store, filter=lambda x: x == 3).as_event() - else: - res = yield Get(store).as_event() - data["one"] = (env.now, res) - - def _proc_two() -> SIMPY_GEN: - if filter: - res = yield Get(store, filter=lambda x: x == 4).as_event() - else: - res = yield Get(store).as_event() - data["two"] = (env.now, res) - - # The bug is that if you stack the requests in an order where the - # first one fails, it's not going to succeed later - def _proc_three() -> SIMPY_GEN: - yield env.timeout(2.0) - yield Put(store, 4).as_event() - yield env.timeout(0.5) - yield Put(store, 3).as_event() - - env.process(_proc_one()) - env.process(_proc_two()) - env.process(_proc_three()) - - env.run() - - data["final"] = list(store.items) - return data - - -def test_monitoring_store_get() -> None: - # This should not be an issue because simpy Store _do_get|put always - # returns None - with EnvironmentContext() as env: - smst = SelfMonitoringStore(env) - data = _store_process(smst, env, filter=False) - assert data.get("one", 0.0) == (0.0, 2) - assert data.get("two", 0.0) == (2.0, 4) - assert smst.items == [3] - - -def test_monitoring_filter_store_get() -> None: - with EnvironmentContext() as env: - smfst = FilterStore(env) - data = _store_process(smfst, env) - assert data.get("one", 0.0) == (2.5, 3) - assert data.get("two", 0.0) == (2.0, 4) - assert data.get("final", [1]) == [2] - - with EnvironmentContext() as env: - smfst = SelfMonitoringFilterStore(env) - data2 = _store_process(smfst, env) - assert data2 == data - - -def test_monitoring_sorted_filter_store_get() -> None: - with EnvironmentContext() as env: - smsfst = SelfMonitoringSortedFilterStore(env) - - data = _store_process(smsfst, env) - assert data.get("one", 0.0) == (2.5, 3) - assert data.get("two", 0.0) == (2.0, 4) - assert data.get("final", [1]) == [2] - - -if __name__ == "__main__": - test_monitoring_container_get() diff --git a/src/upstage_des/test/test_motion.py b/src/upstage_des/test/test_motion.py deleted file mode 100644 index 86791f4..0000000 --- a/src/upstage_des/test/test_motion.py +++ /dev/null @@ -1,897 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Any, Generic, TypeVar, cast - -import pytest -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.geography import Spherical, get_intersection_locations -from upstage_des.motion.cartesian_model import cartesian_linear_intersection as cli -from upstage_des.motion.geodetic_model import analytical_intersection as agi -from upstage_des.motion.geodetic_model import subdivide_intersection as gi -from upstage_des.type_help import TASK_GEN - -LOC = TypeVar("LOC", bound=UP.CartesianLocation | UP.GeodeticLocation) - - -def close(a: float, b: float) -> bool: - """Test if two numbers are close. - - Args: - a (float): number - b (float): other number - - Returns: - bool: if they are close - """ - d = abs(a - b) - return d <= 1e-8 - - -class DummySensor(Generic[LOC]): - """A simple sensor for testing purposes.""" - - def __init__(self, env: SIM.Environment, location: LOC, radius: float = 1.0) -> None: - self.env = env - self.data: list[tuple[Any, float, str]] = [] - self._location = location - self._radius = radius - - def entity_entered_range(self, mover: Any) -> None: - self.data.append((mover, self.env.now, "detect")) - if hasattr(mover, "loc"): - # call the location to record it - mover.loc - - def entity_exited_range(self, mover: Any) -> None: - self.data.append((mover, self.env.now, "end detect")) - if hasattr(mover, "loc"): - # call the location to record it - mover.loc - - @property - def location(self) -> LOC: - return self._location - - @property - def radius(self) -> float: - return self._radius - - -class BadSensor: - """An incomplete sensor for testing purposes.""" - - def __init__(self, env: SIM.Environment, location: tuple[float, ...], radius: float) -> None: - self.env = env - self._location = UP.CartesianLocation(*location) - self._radius = radius - - @property - def location(self) -> UP.CartesianLocation: - return self._location - - @property - def radius(self) -> float: - return self._radius - - -class DummyMover: - """A simple mover for testing purposes.""" - - def __init__(self, env: SIM.Environment) -> None: - self.env = env - self.detect = True - - def _get_detection_state(self) -> str: - return "detect" - - -class RealMover(UP.Actor): - """A more realistic mover that moves in Cartesian Space.""" - - loc = UP.CartesianLocationChangingState(recording=True) - speed = UP.State[float]() - detect = UP.DetectabilityState() - - -class RealGeodeticMover(UP.Actor): - """A more realistic mover that moves in Geodetic Space.""" - - loc = UP.GeodeticLocationChangingState(recording=True) - speed = UP.State[float]() - detect = UP.DetectabilityState() - - -# This is not best practices, but it works for testing -class DoMove(UP.Task): - """A task for movers to move.""" - - waypoints: list[UP.GeodeticLocation] | list[UP.CartesianLocation] = [] - - def task(self, *, actor: RealGeodeticMover | RealMover) -> TASK_GEN: - dist = 0.0 - wayps = [actor.loc] + list(self.waypoints) - for i in range(len(wayps) - 1): - dist += wayps[i + 1] - wayps[i] - time = dist / actor.speed - - actor.activate_location_state( - state="loc", - task=self, - speed=actor.speed, - waypoints=self.waypoints, - ) - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - - def on_interrupt( - self, *, actor: RealGeodeticMover | RealMover, cause: Any - ) -> UP.InterruptStates: - if cause == "Become undetectable": - actor.detect = False - return self.INTERRUPT.IGNORE - return self.INTERRUPT.END - - -L = TypeVar("L") - - -def _create_mover_and_waypoints( - env: SIM.Environment, - mover_type: type, - location_type: type[L], - *waypoints: tuple[float, ...], -) -> tuple[UP.Actor, list[L]]: - mover = cast(UP.Actor, mover_type(env)) - waypoints = [location_type(*waypoint) for waypoint in waypoints] - return mover, waypoints - - -def test_errors() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli) - - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (0, 0, 0), - (1, 1, 0), - ) - - bad_sensor = BadSensor(env, (0.9, 0.9), 0.5) - - with pytest.raises(UP.MotionAndDetectionError): - motion._stop_mover(mover) - - motion._start_mover(mover, speed=1.0, waypoints=waypoints) - with pytest.raises(UP.MotionAndDetectionError): - motion._start_mover(mover, speed=1.0, waypoints=[[2, 2], [3, 3]]) # type: ignore [arg-type] - - with pytest.raises(NotImplementedError): - motion.add_sensor(bad_sensor, "location", "radius") # type: ignore [arg-type] - - -def test_no_interaction_cli() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli) - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (2, 0, 0), - (2, -2, 0), - ) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc) - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run(until=5.0) - - assert abs(env.now - 5.0) < 1e-12 - - assert len(sensor.data) == 0, ( - f"There should be no interaction events, but found: {sensor.data}" - ) - assert not motion._events.get(mover, []) - assert sensor not in motion._in_view.get(mover, {}) - assert motion._debug_log == [], "No log expected for no actions" - - -def test_enter_exit() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(cli) - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (2, 2, 0), - (-2, -2, 0), - ) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc) - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run() - motion._stop_mover(mover) - - assert abs(env.now - 3.828427124746188) < 1e-12 - - assert len(sensor.data) == 2, ( - f"For now, motion manager only has entry event recorded, {sensor.data}" - ) - - assert abs(sensor.data[0][1] - 1.8284271247461907) < 1e-12 - assert sensor.data[0][2] == "detect" - assert abs(sensor.data[1][1] - 3.828427124746188) < 1e-12 - assert sensor.data[1][2] == "end detect" - assert not motion._events.get(mover, []) - assert sensor not in motion._in_view.get(mover, {}) - - -def test_sensor_popup() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli) - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (2, 2, 0), - (-2, -2, 0), - ) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc) - motion._start_mover(mover, 1.0, waypoints) - env.run(until=2) - - motion.add_sensor( - sensor, - ) - env.run() - motion._stop_mover(mover) - - assert abs(env.now - 3.828427124746188) < 1e-12 - - assert len(sensor.data) == 2, ( - f"For now, motion manager only has entry event recorded, {sensor.data}" - ) - - assert abs(sensor.data[0][1] - 2) < 1e-12 - assert sensor.data[0][2] == "detect" - assert abs(sensor.data[1][1] - 3.828427124746188) < 1e-12 - assert sensor.data[1][2] == "end detect" - assert not motion._events.get(mover, []) - assert sensor not in motion._in_view.get(mover, {}) - - -def test_start_inside_exit() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli) - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (0.5, 0.5, 0), - (-2, -2, 0), - ) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc) - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run() - motion._stop_mover(mover) - - assert env.now == 1.7071067811865475 - - assert len(sensor.data) == 2, "Need entry and exit events" - - assert abs(sensor.data[0][1] - 0) < 1e-12 - assert sensor.data[0][2] == "detect" - assert abs(sensor.data[1][1] - 1.7071067811865475) < 1e-12 - assert sensor.data[1][2] == "end detect" - assert not motion._events.get(mover, []) - assert sensor not in motion._in_view.get(mover, {}) - - -def test_enter_end_inside_then_leave() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli) - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (2, 2, 0), - (-0.5, -0.5, 0), - ) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc) - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run() - motion._stop_mover(mover) - - assert env.now == 1.8284271247461907 - - assert len(sensor.data) == 1, "Ending inside means no exit event" - - assert abs(sensor.data[-1][1] - 1.8284271247461907) < 1e-12 - assert sensor in motion._in_view[mover] - assert not motion._events.get(mover, []) - - # have the mover leave and run the clock a bit in case - env.run(until=20) - assert len(sensor.data) == 1, "No new events should be added to the log" - assert abs(sensor.data[-1][1] - 1.8284271247461907) < 1e-12 - assert sensor in motion._in_view[mover] - - motion._start_mover(mover, 1.0, waypoints[::-1]) - env.run() - assert env.now == 21.707106781186546 - assert len(sensor.data) == 2, "Wrong amount of sensor data" - assert abs(sensor.data[-1][1] - 21.707106781186546) < 1e-12 - assert sensor not in motion._in_view[mover] - - -def test_start_inside_end_inside() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli) - mover, waypoints = _create_mover_and_waypoints( - env, - DummyMover, - UP.CartesianLocation, - (0.5, 0.5, 0), - (-0.5, -0.5, 0), - ) - mover = cast(UP.Actor, mover) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc) - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run() - - assert env.now == 0 - - assert len(sensor.data) == 1, f"Only need to see the start recorded: {sensor.data}" - assert sensor.data[0][2] == "detect" - assert sensor in motion._in_view[mover] - - -def test_motion_setup_cli() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover = cast(UP.Actor, DummyMover(env)) - mover_start = UP.CartesianLocation(*[8, 8, 2]) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - - motion.add_sensor(sensor, "location", "radius") - # This isn't how these things are normally called for movers, but - # since it isn't going through a task, it's ok here. - motion._start_mover(mover, 1.0, waypoints) - assert len(motion._events) == 1 - assert len(motion._events[mover]) == 3 - env.run() - assert len(sensor.data) == 6 - matches = [2.343145750507622, 18.343145750507624, 34.268912605325006] - for datum, truth in zip(sensor.data[::2], matches): - assert pytest.approx(truth) == datum[1] - - -def test_late_intersection() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=5.0) - - mover = cast(UP.Actor, DummyMover(env)) - mover_start = UP.CartesianLocation(*[0, 8, 0]) - waypoints = [ - mover_start, - UP.CartesianLocation(*[0, 6, 0]), - ] - motion.add_sensor(sensor) - # This isn't how these things are normally called for movers, but - # since it isn't going through a task, it's ok here. - motion._start_mover(mover, 1.0, waypoints) - env.run() - - -def test_motion_coordination_cli() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - # Test that if the location is a changing state, that it matches up - # when detections happen - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover_start = UP.CartesianLocation(*[8, 8, 2]) - mover = RealMover(name="A Mover", loc=mover_start, speed=1, detect=True) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - task.run(actor=mover) - - motion.add_sensor(sensor) - - env.run() - assert mover.loc == waypoints[-1] - assert len(motion._debug_data[mover]) == 3 - for i, data in enumerate(motion._debug_data[mover]): - sense, kinds, times, inters = data - mover_at_time_0 = [x[1] for x in mover._state_histories["loc"] if close(x[0], times[0])] - mover_at_time_1 = [x[1] for x in mover._state_histories["loc"] if close(x[0], times[1])] - assert mover_at_time_0 - assert mover_at_time_1 - assert close(mover_at_time_0[0], inters[0]) - assert close(mover_at_time_1[0], inters[1]) - - -def test_background_motion() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover_start = UP.CartesianLocation(*[8, 8, 2]) - mover = RealMover(name="A Mover", loc=mover_start, speed=1, detect=True) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - task.run(actor=mover) - - motion.add_sensor(sensor) - # no need to start the mover this time - env.run() - assert mover.loc == waypoints[-1] - # The motion manager needs to see the mover 3 times, and - # so does the sensor - assert len(motion._debug_data[mover]) == 3 - assert len(sensor.data) == 6 - - -def test_background_rehearse() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - - # This is the key change relative to the above - flyer_start = UP.CartesianLocation(*[8, 8, 2]) - flyer = RealMover(name="A Mover", loc=flyer_start, speed=1, detect=True) - waypoints = [ - flyer_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - - flyer_clone = task.rehearse(actor=flyer) - - env.run() - assert env.now == 0, "No time should pass for rehearsal" - assert flyer.loc == waypoints[0] - # The motion manager shouldn't see anything - assert flyer not in motion._debug_data - assert flyer_clone not in motion._debug_data - - -def test_interrupt_clean() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover_start = UP.CartesianLocation(*[8, 8, 2]) - mover = RealMover(name="A Mover", loc=mover_start, speed=1, detect=True, debug_log=True) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - proc = task.run(actor=mover) - - motion.add_sensor(sensor) - # no need to start the mover this time - env.run(until=25) - proc.interrupt(cause="Stop") - env.run() - - # The motion manager needs to see the mover 3 times, and - # the sensor will see it double that for entry/exit - assert len(motion._debug_data[mover]) == 3 - assert len(sensor.data) == 3 - # the two messages for cancelling the notifications - assert len(motion._debug_log) == 5 - assert all( - log_entry["event"] == "Scheduling sensor detecting mover" - for log_entry in motion._debug_log[:3] - ) - assert all(line["mover"] is mover for line in motion._debug_log) - # the order of these two may switch.. - msgs = [ - "Detection of a mover cancelled before exit", - "Detection of a mover cancelled before entry", - ] - events = [motion._debug_log[i]["event"] for i in [3, 4]] - assert len(set(events)) == 2, "Need two unique event descriptions" - assert all(x in msgs for x in events), "Need both types exactly" - # the mover should be stopped - assert mover not in motion._movers - - -def test_undetectable_cli() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover_start = UP.CartesianLocation(*[8, 8, 2]) - mover = RealMover(name="A Mover", loc=mover_start, speed=1, debug_log=True, detect=True) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - proc = task.run(actor=mover) - - motion.add_sensor(sensor) - # no need to start the mover this time - env.run(until=25) - - # check that the motion manager has the right data - assert mover in motion._in_view, "Mover not found in progress" - proc.interrupt(cause="Become undetectable") - env.run() - - assert sensor.data[-1] == (mover, 25, "end detect") - - -def test_redetectable() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover_start = UP.CartesianLocation(*[8, 8, 2]) - mover = RealMover(name="A Mover", loc=mover_start, speed=1, debug_log=True, detect=False) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - task.run(actor=mover) - - motion.add_sensor(sensor) - # no need to start the mover this time - env.run(until=25) - with pytest.warns(UserWarning, match="Setting DetectabilityState to True while*"): - mover.detect = True - - -def test_undetectable_after() -> None: - with UP.EnvironmentContext() as env: - UP.add_stage_variable("distance_units", "m") - motion = UP.SensorMotionManager(cli, debug=True) - UP.add_stage_variable("motion_manager", motion) - loc = UP.CartesianLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=10.0) - - mover_start = UP.CartesianLocation(*[8, 8, 2]) - mover = RealMover(name="A Mover", loc=mover_start, speed=1, debug_log=True, detect=True) - waypoints = [ - mover_start, - UP.CartesianLocation(*[-8, 8, 2]), - UP.CartesianLocation(*[-8, -8, 2]), - UP.CartesianLocation(*[8, -8, 0]), - ] - task = DoMove() - task.waypoints = waypoints[1:] - proc = task.run(actor=mover) - - motion.add_sensor(sensor) - # no need to start the mover this time - env.run(until=30) - - # check that the motion manager has the right data - assert mover in motion._in_view, "Mover not found in progress" - proc.interrupt(cause="Become undetectable") - env.run() - - assert (mover, 30, "end detect") not in sensor.data - assert len(sensor.data) == 4 - - -def test_motion_setup_gi() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(gi, debug=True) - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "m") - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("intersection_model", get_intersection_locations) - loc = UP.GeodeticLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=150.0) - - geo_mover = cast(UP.Actor, DummyMover(env)) - t = 2 - geo_mover_start = UP.GeodeticLocation(*[t, t, 4000]) - waypoints = [ - geo_mover_start, - UP.GeodeticLocation(*[-t, t, 4000]), - UP.GeodeticLocation(*[-t, -t, 4000]), - UP.GeodeticLocation(*[t, -t, 0]), - ] - - motion.add_sensor(sensor) - motion._start_mover(geo_mover, 1.0, waypoints) - assert len(motion._events) == 1 - assert len(motion._events[geo_mover]) == 3 - env.run() - assert len(sensor.data) == 6 - matches = [30.59361210120766, 271.030439713508, 511.28446115022086] - for datum, truth in zip(sensor.data[::2], matches): - assert pytest.approx(truth, abs=0.001) == datum[1] - - -def test_no_interaction_gi() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(gi, debug=True) - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "ft") - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("intersection_model", get_intersection_locations) - - motion = UP.SensorMotionManager(gi) - loc = UP.GeodeticLocation(*[90, 40, 0]) - sensor = DummySensor(env, loc, 1.0) - - mover = cast(UP.Actor, DummyMover(env)) - waypoints = [ - UP.GeodeticLocation(0, 10, 0), - UP.GeodeticLocation(10, 10, 0), - ] - - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run(until=5.0) - - assert abs(env.now - 5.0) < 1e-12 - - assert len(sensor.data) == 0, ( - f"There should be no interaction events, but found: {sensor.data}" - ) - - motion._stop_mover(mover) - - -def test_motion_coordination_gi() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(gi, debug=True) - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "m") - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("intersection_model", get_intersection_locations) - UP.add_stage_variable("motion_manager", motion) - loc = UP.GeodeticLocation(0, 0, 0) - sensor = DummySensor(env, loc, radius=150.0) - - t = 2 - geo_mover_start = UP.GeodeticLocation(*[t, t, 4000]) - geo_mover = RealGeodeticMover(name="Mover", loc=geo_mover_start, speed=1, detect=True) - - waypoints = [ - geo_mover_start, - UP.GeodeticLocation(*[-t, t, 4000]), - UP.GeodeticLocation(*[-t, -t, 4000]), - UP.GeodeticLocation(*[t, -t, 0]), - ] - - task = DoMove() - task.waypoints = waypoints[1:] - task.run(actor=geo_mover) - - motion.add_sensor(sensor) - - env.run() - assert abs(geo_mover.loc - waypoints[-1]) <= 1e-12 - assert len(motion._debug_data[geo_mover]) == 3 - for i, data in zip([1, 3, 5], motion._debug_data[geo_mover]): - sense, kinds, times, inters = data - mover_at_time_0 = [ - x[1] for x in geo_mover._state_histories["loc"] if close(x[0], times[0]) - ] - mover_at_time_1 = [ - x[1] for x in geo_mover._state_histories["loc"] if close(x[0], times[1]) - ] - assert mover_at_time_0 - assert mover_at_time_1 - assert close(mover_at_time_0[0], inters[0]) - assert close(mover_at_time_1[0], inters[1]) - - -def test_motion_setup_agi() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(agi, debug=True) - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "m") - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("intersection_model", get_intersection_locations) - UP.add_stage_variable("motion_manager", motion) - sensor = DummySensor(env, UP.GeodeticLocation(0, 0, 0), radius=150.0) - - geo_mover = cast(UP.Actor, DummyMover(env)) - t = 2 - geo_mover_start = UP.GeodeticLocation(*[t, t, 4000]) - waypoints = [ - geo_mover_start, - UP.GeodeticLocation(*[-t, t, 4000]), - UP.GeodeticLocation(*[-t, -t, 4000]), - UP.GeodeticLocation(*[t, -t, 0]), - ] - - motion.add_sensor(sensor) - motion._start_mover(geo_mover, 1.0, waypoints) - assert len(motion._events) == 1 - assert len(motion._events[geo_mover]) == 3 - env.run() - assert len(sensor.data) == 6 - matches = [30.511, 270.967, 511.207] - for datum, truth in zip(sensor.data[::2], matches): - assert pytest.approx(truth, abs=0.1) == datum[1] - - -def test_no_interaction_agi() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(agi, debug=True) - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "m") - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("intersection_model", get_intersection_locations) - UP.add_stage_variable("motion_manager", motion) - - motion = UP.SensorMotionManager(agi) - sensor = DummySensor(env, UP.GeodeticLocation(0, 0, 0)) - UP.GeodeticLocation(*[0, 0, 0]) - - mover = cast(UP.Actor, DummyMover(env)) - waypoints = [ - UP.GeodeticLocation(0, 10, 0), - UP.GeodeticLocation(10, 10, 0), - ] - - motion.add_sensor( - sensor, - ) - motion._start_mover(mover, 1.0, waypoints) - env.run(until=5.0) - - assert abs(env.now - 5.0) < 1e-12 - - assert len(sensor.data) == 0, ( - f"There should be no interaction events, but found: {sensor.data}" - ) - - motion._stop_mover(mover) - - -def test_motion_coordination_agi() -> None: - with UP.EnvironmentContext() as env: - motion = UP.SensorMotionManager(agi, debug=True) - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "m") - UP.add_stage_variable("distance_units", "nmi") - UP.add_stage_variable("intersection_model", get_intersection_locations) - UP.add_stage_variable("motion_manager", motion) - - sensor = DummySensor(env, UP.GeodeticLocation(0, 0, 0), radius=150.0) - - t = 2 - geo_mover_start = UP.GeodeticLocation(*[t, t, 4000]) - geo_mover = RealGeodeticMover(name="Mover", loc=geo_mover_start, speed=1, detect=True) - - waypoints = [ - geo_mover_start, - UP.GeodeticLocation(*[-t, t, 4000]), - UP.GeodeticLocation(*[-t, -t, 4000]), - UP.GeodeticLocation(*[t, -t, 0]), - ] - - task = DoMove() - task.waypoints = waypoints[1:] - task.run(actor=geo_mover) - - motion.add_sensor(sensor) - - env.run() - assert abs(geo_mover.loc - waypoints[-1]) <= 1e-12 - assert len(motion._debug_data[geo_mover]) == 3 - for i, data in zip([1, 3, 5], motion._debug_data[geo_mover]): - sense, kinds, times, inters = data - mover_at_time_0 = [ - x[1] for x in geo_mover._state_histories["loc"] if close(x[0], times[0]) - ] - mover_at_time_1 = [ - x[1] for x in geo_mover._state_histories["loc"] if close(x[0], times[1]) - ] - assert mover_at_time_0 - assert mover_at_time_1 - assert abs(mover_at_time_0[0] - inters[0]) <= 0.5 # nmi - assert abs(mover_at_time_1[0] - inters[1]) <= 0.7 # nmi - - -def test_analytical_intersection() -> None: - with UP.EnvironmentContext(): - UP.add_stage_variable("stage_model", Spherical) - UP.add_stage_variable("altitude_units", "m") - UP.add_stage_variable("distance_units", "nmi") - - start = UP.GeodeticLocation(33.67009544379275, -84.59178543542892, 5_000) - finish = UP.GeodeticLocation(33.871012616336344, -84.16331866903882, 5_000) - middle = UP.GeodeticLocation(33.7774620987044, -84.38304521590554, 4_000) - - res = agi(start, finish, 200.0, middle, 200.0) - intersections, times, types, path_time = res - assert types == ["START_INSIDE", "END_INSIDE"] diff --git a/src/upstage_des/test/test_network_qol.py b/src/upstage_des/test/test_network_qol.py deleted file mode 100644 index caa711d..0000000 --- a/src/upstage_des/test/test_network_qol.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - - -class Act(UP.Actor): - state = UP.State[int](default=0) - - -class Do(UP.Task): - def task(self, *, actor: Act) -> TASK_GEN: - yield UP.Wait(2) - actor.state = actor.state + 1 - - -class Do2(UP.Task): - def task(self, *, actor: Act) -> TASK_GEN: - yield UP.Wait(3) - actor.state = actor.state + 3 - - -def test_single_loop_and_names() -> None: - with UP.EnvironmentContext() as env: - factory = UP.TaskNetworkFactory.from_single_looping("example", Do) - - actor = Act(name="a thing") - - suggest = actor.suggest_network_name(factory) - assert suggest == "example" - new_net = factory.make_network(other_name=suggest) - actor.add_task_network(new_net) - - suggest = actor.suggest_network_name(factory) - assert suggest == "example_1" - new_net = factory.make_network(other_name=suggest) - actor.add_task_network(new_net) - - actor.start_network_loop("example", init_task_name="Do") - actor.start_network_loop("example_1", init_task_name="Do") - - env.run(until=3) - - assert actor.state == 2 - - assert actor.has_task_network("example") - assert actor.has_task_network("example_1") - - actor.delete_task_network("example") - actor.delete_task_network("example_1") - - assert not actor.has_task_network("example") - assert not actor.has_task_network("example_1") - - env.run(until=5) - - assert actor.state == 4 - - -def test_other_inits() -> None: - with UP.EnvironmentContext() as env: - factory = UP.TaskNetworkFactory.from_ordered_terminating("example", [Do, Do2]) - actor = Act(name="a thing") - - new_net = factory.make_network() - actor.add_task_network(new_net) - - actor.start_network_loop("example", init_task_name="Do2") - - env.run(until=4) - - assert actor.state == 3 - - task = actor.get_running_task("example") - assert task is not None and "TERMINATING" in task.name - - with UP.EnvironmentContext() as env: - factory = UP.TaskNetworkFactory.from_ordered_loop("example", [Do, Do2]) - actor = Act(name="a thing") - - new_net = factory.make_network() - actor.add_task_network(new_net) - - actor.start_network_loop("example", init_task_name="Do") - - env.run(until=6) - - assert actor.state == 4 diff --git a/src/upstage_des/test/test_nucleus.py b/src/upstage_des/test/test_nucleus.py deleted file mode 100644 index 43b135e..0000000 --- a/src/upstage_des/test/test_nucleus.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Any - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - - -class Dummy(UP.Actor): - number = UP.State[float]() - results = UP.State[int](default=0) - - -class Example(UP.Task): - def task(self, *, actor: Dummy) -> TASK_GEN: - yield UP.Wait(actor.number) - actor.number /= 2 - - -class OtherExample(UP.Task): - def task(self, *, actor: Dummy) -> TASK_GEN: - actor.results += 1 - yield UP.Wait(100) - - def on_interrupt(self, *, actor: Dummy, cause: Any) -> UP.InterruptStates: - super().on_interrupt(actor=actor, cause=cause) - return self.INTERRUPT.RESTART - - -fact = UP.TaskNetworkFactory( - "example", - {"Runner": Example}, - {"Runner": UP.TaskLinks(default="Runner", allowed=["Runner"])}, -) - -fact2 = UP.TaskNetworkFactory( - "side", - {"Side": OtherExample}, - {"Side": UP.TaskLinks(default="Side", allowed=["Side"])}, -) - - -def test_creation() -> None: - with UP.EnvironmentContext() as env: - actor = Dummy(name="example", number=10) - nuc = UP.TaskNetworkNucleus(actor=actor) - task_net = fact.make_network() - actor.add_task_network(task_net) - nuc.add_network(task_net, []) - # start the task network - actor.start_network_loop("example", init_task_name="Runner") - env.run(until=18) - assert actor.number == 1.25 - - -def test_with_interrupt() -> None: - with UP.EnvironmentContext() as env: - actor = Dummy(name="example", number=10, results=0) - nuc = UP.TaskNetworkNucleus(actor=actor) - - task_net = fact.make_network() - actor.add_task_network(task_net) - nuc.add_network(task_net, []) - - task_net_2 = fact2.make_network() - actor.add_task_network(task_net_2) - nuc.add_network(task_net_2, ["number"]) - - actor.start_network_loop("example", init_task_name="Runner") - actor.start_network_loop("side", init_task_name="Side") - - env.run(until=15) - assert actor.results == 2 diff --git a/src/upstage_des/test/test_nucleus_state_share/__init__.py b/src/upstage_des/test/test_nucleus_state_share/__init__.py deleted file mode 100644 index 22a7819..0000000 --- a/src/upstage_des/test/test_nucleus_state_share/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Tests for nucleus and state sharing.""" - -from .flyer import Flyer, flyer_refuel_factory, mission_plan_net -from .mothership import Mothership, crew_factory, give_fuel_factory -from .mover import Mover, fly_end_factory - -__all__ = [ - "Flyer", - "flyer_refuel_factory", - "mission_plan_net", - "Mover", - "fly_end_factory", - "Mothership", - "crew_factory", - "give_fuel_factory", -] diff --git a/src/upstage_des/test/test_nucleus_state_share/flyer.py b/src/upstage_des/test/test_nucleus_state_share/flyer.py deleted file mode 100644 index 809b29f..0000000 --- a/src/upstage_des/test/test_nucleus_state_share/flyer.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - -from .mothership import Mothership -from .mover import Mover - - -class Flyer(Mover): - fuel_capacity = UP.State[float](valid_types=(float,), frozen=True) - fuel_draw = UP.State[float](valid_types=(int, float), frozen=True) - messages = UP.ResourceState[SIM.Store](default=SIM.Store, valid_types=(SIM.Store,)) - approach = UP.State[bool](default=False, valid_types=(bool,), recording=True) - - -class MissionPlanning(UP.Task): - def task(self, *, actor: Flyer) -> TASK_GEN: - # Figure out the time to reach the right waypoint - # Schedule the approach network - # and a speed change for that time. - # Also, kick off the flying. - refuel_point = self.get_actor_knowledge(actor, "meetup", must_exist=True) - time_to_point = (actor.location - refuel_point) / actor.speed - yield UP.Wait(time_to_point) - actor.approach = True - - -mission_plan_net = UP.TaskNetworkFactory.from_single_terminating("plan", MissionPlanning) - - -class ApproachWait(UP.Task): - def task(self, *, actor: Flyer) -> TASK_GEN: - yield UP.Event() - - def on_interrupt(self, *, actor: Flyer, cause: UP.NucleusInterrupt) -> UP.InterruptStates: - assert cause.state_name == "approach" - return self.INTERRUPT.END - - -class ApproachMothership(UP.Task): - def task(self, *, actor: Flyer) -> TASK_GEN: - the_mothership = self.get_actor_knowledge(actor, "mothership", must_exist=True) - refuel_speed = self.get_actor_knowledge(actor, "refuel_speed", must_exist=True) - actor.speed = refuel_speed # NUCLEUS INTERACTION - yield UP.Put(the_mothership.messages, (actor, -actor.fuel_draw)) - proceed = yield UP.Get(actor.messages) - assert proceed == "GO" - - -class Refuel(UP.Task): - def task(self, *, actor: Flyer) -> TASK_GEN: - # ignore any particular timing to "full fuel" - needed = actor.fuel_capacity - actor.fuel - # we're here if we got the message to go - actor.activate_state( - state="fuel", - task=self, - rate=actor.fuel_draw, - ) - time = needed / actor.fuel_draw - yield UP.Wait(time) - actor.deactivate_state(state="fuel", task=self) - the_mothership: Mothership = self.get_actor_knowledge(actor, "mothership", must_exist=True) - yield UP.Put(the_mothership.messages, (actor, 0)) - self.set_actor_knowledge(actor, "done_refueling", True, overwrite=True) - - -flyer_refuel_factory = UP.TaskNetworkFactory.from_ordered_loop( - "flyer_refuel", - [ApproachWait, ApproachMothership, Refuel], -) diff --git a/src/upstage_des/test/test_nucleus_state_share/mothership.py b/src/upstage_des/test/test_nucleus_state_share/mothership.py deleted file mode 100644 index 6d9d9ed..0000000 --- a/src/upstage_des/test/test_nucleus_state_share/mothership.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Any - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - -from .mover import Mover - - -class Mothership(Mover): - fuel_ports_in_use = UP.State[int](valid_types=(int,)) - fuel_ports_max = UP.State[int](valid_types=(int,), frozen=True) - messages = UP.ResourceState[UP.SelfMonitoringStore]( - default=UP.SelfMonitoringStore, valid_types=(UP.SelfMonitoringStore, SIM.Store) - ) - - -class DispenseFuel(UP.Task): - def task(self, *, actor: Mothership) -> TASK_GEN: - draws = self.get_actor_knowledge(actor, "fuel_users", must_exist=False) - draws = {} if draws is None else draws - total_draw = sum(draws.values()) - actor.activate_state( - state="fuel", - task=self, - rate=total_draw, - ) - # Infinite wait! - # We use Nucleus to go to the interrupt. - yield UP.Event() - - def on_interrupt(self, *, actor: Mothership, cause: Any) -> UP.InterruptStates: - # No matter what, restart - return self.INTERRUPT.RESTART - - -give_fuel_factory = UP.TaskNetworkFactory.from_single_looping("GiveFuel", DispenseFuel) - - -class CrewMember(UP.Task): - def _user_add(self, actor: Mothership, vehicle: Mover, add: float) -> int: - know = self.get_actor_knowledge(actor, "fuel_users") - know = {} if know is None else know - know[vehicle] = add - self.set_actor_knowledge(actor, "fuel_users", know, overwrite=True) - return len(know) - - def _user_remove(self, actor: Mothership, vehicle: Mover) -> int: - know = self.get_actor_knowledge(actor, "fuel_users") - know = {} if know is None else know - del know[vehicle] - self.set_actor_knowledge(actor, "fuel_users", know, overwrite=True) - return len(know) - - def task(self, *, actor: Mothership) -> TASK_GEN: - # receive a message that someone is ready, then update fuel_users - msg = yield UP.Get(actor.messages) - vehicle, draw = msg - if draw == 0: - ports = self._user_remove(actor, vehicle) - else: - ports = self._user_add(actor, vehicle, draw) - # kill some time to send a message back - yield UP.Wait(5 / 60) - yield UP.Put(vehicle.messages, "GO") - actor.fuel_ports_in_use = ports - - -crew_factory = UP.TaskNetworkFactory.from_single_looping("crew", CrewMember) diff --git a/src/upstage_des/test/test_nucleus_state_share/mover.py b/src/upstage_des/test/test_nucleus_state_share/mover.py deleted file mode 100644 index 0dbcbe5..0000000 --- a/src/upstage_des/test/test_nucleus_state_share/mover.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Any - -import upstage_des.api as UP -from upstage_des.type_help import TASK_GEN - - -class Mover(UP.Actor): - fuel = UP.SharedLinearChangingState(recording=True) - fuel_burn = UP.State[float](valid_types=(float,), frozen=True) - location = UP.CartesianLocationChangingState(recording=True) - speed = UP.State[float](valid_types=(float, int), recording=True) - - def get_distance( - self, waypoints: list[UP.GeodeticLocation] | list[UP.CartesianLocation] - ) -> float: - d = waypoints[0] - self.location - for i in range(1, len(waypoints)): - d += waypoints[i] - waypoints[i - 1] - return d - - -class Fly(UP.Task): - def task(self, *, actor: Mover) -> TASK_GEN: - destinations = list(self.get_actor_knowledge(actor, "destinations")) - self.clear_actor_knowledge(actor, "destinations") - actor.activate_state( - state="location", - task=self, - speed=actor.speed, - waypoints=destinations, - ) - actor.activate_state( - state="fuel", - task=self, - rate=actor.fuel_burn, - ) - dist = actor.get_distance(destinations) - time = dist / actor.speed - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - - def on_interrupt(self, *, actor: Mover, cause: Any) -> UP.InterruptStates: - # Allow subclassing to run a check prior to this - # and input "restart" if they want. - reason = None - if isinstance(cause, UP.NucleusInterrupt): - if cause.state_name == "speed": - reason = "restart" - if reason == "restart": - rem_wypts = actor.get_remaining_waypoints( - location_state="location", - ) - self.set_actor_knowledge( - actor, - "destinations", - rem_wypts, - overwrite=True, - ) - return self.INTERRUPT.RESTART - return self.INTERRUPT.END - - -fly_factory = UP.TaskNetworkFactory.from_single_looping("Fly", Fly) -fly_end_factory = UP.TaskNetworkFactory.from_single_terminating("Fly", Fly) diff --git a/src/upstage_des/test/test_nucleus_state_share/test_refuel_example.py b/src/upstage_des/test/test_nucleus_state_share/test_refuel_example.py deleted file mode 100644 index e6fcba4..0000000 --- a/src/upstage_des/test/test_nucleus_state_share/test_refuel_example.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.type_help import SIMPY_GEN - -from .flyer import Flyer, flyer_refuel_factory, mission_plan_net -from .mothership import Mothership, crew_factory, give_fuel_factory -from .mover import fly_end_factory - - -def build_sim() -> tuple[float, Mothership]: - MEETING_POINT = UP.CartesianLocation(0, 0) - TRAVERSE_TO_POINT = UP.CartesianLocation(200, 0) - START_POINT = UP.CartesianLocation(200, 200) - - mothership = Mothership( - name="Fuel Giver", - fuel=1000.0, - fuel_burn=-100.0, - speed=200.0, - location=START_POINT, - fuel_ports_in_use=0, - fuel_ports_max=4, - ) - fly_net = fly_end_factory.make_network() - give_fuel_net = give_fuel_factory.make_network() - crew_net = crew_factory.make_network() - for net in [fly_net, give_fuel_net, crew_net]: - mothership.add_task_network(net) - # assume that the first entered task name is the one we want - mothership.start_network_loop( - net.name, - list(net.task_classes.keys())[0], - ) - - # give some knowledge - mothership.set_knowledge( - "destinations", - [MEETING_POINT, TRAVERSE_TO_POINT, START_POINT], - ) - - # nucleus - mothership_nucleus = UP.TaskNetworkNucleus(actor=mothership) - mothership_nucleus.add_network( - fly_net, - [ - "speed", - ], - ) - mothership_nucleus.add_network(give_fuel_net, ["fuel_ports_in_use"]) - - total_dist = ( - (START_POINT - TRAVERSE_TO_POINT) - + (TRAVERSE_TO_POINT - MEETING_POINT) - + (START_POINT - MEETING_POINT) - ) - return total_dist, mothership - - -def speed_change( - env: SIM.Environment, - vehicle: Mothership, - new_speed: float, - time: float, -) -> SIMPY_GEN: - yield env.timeout(time) - vehicle.speed = new_speed - - -def add_draw( - env: SIM.Environment, - vehicle: Mothership, - amount: float, - time_to: float, - time_on: float, -) -> SIMPY_GEN: - class Dummy(UP.Actor): - messages = UP.ResourceState[SIM.Store](default=SIM.Store) - - d = Dummy(name="a_vehicle") - yield env.timeout(time_to) - yield vehicle.messages.put((d, amount)) - yield env.timeout(time_on) - yield vehicle.messages.put((d, 0)) - - -def test_nothing_added() -> None: - with UP.EnvironmentContext() as env: - total_dist, mothership = build_sim() - t = total_dist / 200 - env.run() - assert pytest.approx(env.now) == t - assert pytest.approx(mothership.fuel) == 1000 - 100 * t - - -def test_fueling() -> None: - with UP.EnvironmentContext() as env: - _, mothership = build_sim() - - env.process(speed_change(env, mothership, 150, 1.3)) - env.process(add_draw(env, mothership, -100, 0.5, 1.5)) - env.process(add_draw(env, mothership, -200, 1.0, 2)) - - env.run() - - # final time must be larger (~3.4 for no speed change) - assert pytest.approx(4.1189514164974605) == env.now - - # final fuel level - assert pytest.approx(63.104858350253835) == mothership.fuel - - -# Fake mothership -class Dummy(UP.Actor): - messages = UP.ResourceState[SIM.Store](default=SIM.Store) - - -def figher_build() -> tuple[Flyer, Dummy]: - MEETING_POINT = UP.CartesianLocation(0, 0) - TRAVERSE_TO_POINT = UP.CartesianLocation(200, 0) - START_POINT = UP.CartesianLocation(-200, -200) - - flyer = Flyer( - name="Flyer Thing", - fuel=1000.0, - fuel_capacity=1500.0, - fuel_burn=-100.0, - fuel_draw=500, - speed=300.0, - location=START_POINT, - debug_log=True, - ) - - fly_net = fly_end_factory.make_network() - get_fuel_net = flyer_refuel_factory.make_network() - plan_net = mission_plan_net.make_network() - for net in [fly_net, get_fuel_net, plan_net]: - flyer.add_task_network(net) - # assume that the first entered task name is the one we want - flyer.start_network_loop( - net.name, - list(net.task_classes.keys())[0], - ) - - # give some knowledge - flyer.set_knowledge( - "destinations", - [MEETING_POINT, TRAVERSE_TO_POINT, START_POINT], - ) - - d = Dummy(name="the_mothership") - # env.process(dummy_mothership_proc(env, d)) - - flyer.set_knowledge("mothership", d) - flyer.set_knowledge("refuel_speed", 150) - flyer.set_knowledge("meetup", MEETING_POINT) - - # nucleus - flyer_nucleus = UP.TaskNetworkNucleus(actor=flyer) - flyer_nucleus.add_network( - fly_net, - [ - "speed", - ], - ) - flyer_nucleus.add_network(get_fuel_net, ["approach"]) - return flyer, d - - -def dummy_mothership_proc(mothership: Dummy) -> SIMPY_GEN: - msg = yield mothership.messages.get() - sendback = msg[0] - yield sendback.messages.put("GO") - - -def test_flyer_nothing() -> None: - with UP.EnvironmentContext() as env: - flyer, _ = figher_build() - env.run() - t = 5.257566344915117 - assert pytest.approx(env.now) == t - assert pytest.approx(flyer.fuel) == 1000 - 100 * t - - -def test_flyer_refuel() -> None: - with UP.EnvironmentContext() as env: - flyer, mothership = figher_build() - env.process(dummy_mothership_proc(mothership)) - env.run() - t = 5.257566344915117 - assert pytest.approx(env.now) == t - assert pytest.approx(flyer.fuel) == 1068.5242696666946 - - -def test_full_fuelinging() -> None: - with UP.EnvironmentContext() as env: - MEETING_POINT = UP.CartesianLocation(0, 0) - TRAVERSE_TO_POINT = UP.CartesianLocation(200, 0) - START_POINT = UP.CartesianLocation(200, 200) - - mothership = Mothership( - name="Fuel Delivery", - fuel=1000.0, - fuel_burn=-100.0, - speed=200.0, - location=START_POINT, - fuel_ports_in_use=0, - fuel_ports_max=4, - ) - fly_net = fly_end_factory.make_network() - give_fuel_net = give_fuel_factory.make_network() - crew_net = crew_factory.make_network() - for net in [fly_net, give_fuel_net, crew_net]: - mothership.add_task_network(net) - # assume that the first entered task name is the one we want - mothership.start_network_loop( - net.name, - list(net.task_classes.keys())[0], - ) - - # give some knowledge - mothership.set_knowledge( - "destinations", - [MEETING_POINT, TRAVERSE_TO_POINT, START_POINT], - ) - - # nucleus - mothership_nucleus = UP.TaskNetworkNucleus(actor=mothership) - mothership_nucleus.add_network( - fly_net, - [ - "speed", - ], - ) - mothership_nucleus.add_network(give_fuel_net, ["fuel_ports_in_use"]) - - MEETING_POINT = UP.CartesianLocation(0, 0) - TRAVERSE_TO_POINT = UP.CartesianLocation(200, 0) - START_POINT = UP.CartesianLocation(-200, -200) - - flyer = Flyer( - name="Flying Thing", - fuel=1000.0, - fuel_capacity=1500.0, - fuel_burn=-100.0, - fuel_draw=500, - speed=300.0, - location=START_POINT, - debug_log=True, - ) - - fly_net = fly_end_factory.make_network() - get_fuel_net = flyer_refuel_factory.make_network() - plan_net = mission_plan_net.make_network() - for net in [fly_net, get_fuel_net, plan_net]: - flyer.add_task_network(net) - # assume that the first entered task name is the one we want - flyer.start_network_loop( - net.name, - list(net.task_classes.keys())[0], - ) - - # give some knowledge - flyer.set_knowledge( - "destinations", - [MEETING_POINT, TRAVERSE_TO_POINT, START_POINT], - ) - - flyer.set_knowledge("mothership", mothership) - # This speed doesn't match the mothership, but - # the test numbers are tuned for it - flyer.set_knowledge("refuel_speed", 150) - flyer.set_knowledge("meetup", MEETING_POINT) - - # nucleus - flyer_nucleus = UP.TaskNetworkNucleus(actor=flyer) - flyer_nucleus.add_network( - fly_net, - [ - "speed", - ], - ) - flyer_nucleus.add_network(get_fuel_net, ["approach"]) - - env.run() - - # The total fuel to start is: 2000 - flyer_flight_t = 5.257566344915117 - flyer_burn = 100 * flyer_flight_t - mothership_flight_t = 682.842712474619 / 200 - mothership_burn = 100 * mothership_flight_t - total_burn = mothership_burn + flyer_burn - fuel_left = mothership.fuel + flyer.fuel - assert pytest.approx(fuel_left) == 2000 - total_burn diff --git a/src/upstage_des/test/test_parallel_task_network.py b/src/upstage_des/test/test_parallel_task_network.py deleted file mode 100644 index 8b31ea3..0000000 --- a/src/upstage_des/test/test_parallel_task_network.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.api import Task -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class ParallelTest(UP.Actor): - comms = UP.State[SIM.Store]() - logger = UP.State[list]() - internal = UP.State[SIM.Store]() - working = UP.State[bool]() - - -class TaskOne(Task): - def task(self, *, actor: ParallelTest) -> TASK_GEN: - actor.working = not actor.working - actor._lock_state(state="working", task=self) - thing = yield UP.Get(actor.internal) - actor.logger.append((actor.env.now, thing, actor.working)) - actor._unlock_state(state="working", task=self) - - -class TaskTwo(Task): - def task(self, *, actor: ParallelTest) -> TASK_GEN: - other = yield UP.Get(actor.comms) - actor.logger.append((actor.env.now, "Put the message", actor.working)) - yield UP.Put(actor.internal, other) - - -def test_parallel_looping() -> None: - with UP.EnvironmentContext() as env: - net_1_classes = {"Task": TaskOne} - net_1_links = {"Task": UP.TaskLinks(default="Task", allowed=["Task"])} - net_2_classes = {"Task": TaskTwo} - net_2_links = {"Task": UP.TaskLinks(default="Task", allowed=["Task"])} - - tn1 = UP.TaskNetwork("InternalGet", net_1_classes, net_1_links) - tn2 = UP.TaskNetwork("ExternalGet", net_2_classes, net_2_links) - - def proc(env: SIM.Environment, actor: ParallelTest, thing: str) -> SIMPY_GEN: - yield env.timeout(1.3) - yield actor.comms.put(thing) - yield env.timeout(2.2) - yield actor.comms.put(thing) - - pt = ParallelTest( - name="Parallel_Actor", - comms=SIM.Store(env), - internal=SIM.Store(env), - logger=[], - working=False, - ) - - pt.add_task_network(tn1) - pt.add_task_network(tn2) - - pt.start_network_loop("InternalGet", init_task_name="Task") - pt.start_network_loop("ExternalGet", init_task_name="Task") - env.process(proc(env, pt, "the msg")) - env.run(until=1.0) - running = pt.get_running_tasks() - assert len(running) == 2 - assert "InternalGet" in running - assert running["InternalGet"].name == "Task" - assert "ExternalGet" in running - assert running["ExternalGet"].name == "Task" - - tqs = pt.get_all_task_queues() - assert "InternalGet" in tqs - assert "ExternalGet" in tqs - - env.run() - - assert len(pt.logger) == 4 - expected_log = [ - (1.3, "Put the message", True), - (1.3, "the msg", True), - (3.5, "Put the message", False), - (3.5, "the msg", False), - ] - for el, al in zip(expected_log, pt.logger): - assert el == al diff --git a/src/upstage_des/test/test_request_cancel.py b/src/upstage_des/test/test_request_cancel.py deleted file mode 100644 index c21ee37..0000000 --- a/src/upstage_des/test/test_request_cancel.py +++ /dev/null @@ -1,124 +0,0 @@ -from typing import Any - -from simpy import Store, Timeout -from simpy.resources.store import StorePut - -import upstage_des.api as UP -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class Storing(UP.Actor): - the_store = UP.ResourceState[Store](default=Store) - result = UP.State[Any](default="First") - - -class Getting(UP.Task): - def task(self, *, actor: Storing) -> TASK_GEN: - """Get""" - getter = UP.Get(actor.the_store) - yield getter - actor.result = getter.get_value() - - def on_interrupt(self, *, actor: Storing, cause: Any) -> UP.InterruptStates: - return UP.InterruptStates.END - - -class Putting(UP.Task): - def task(self, *, actor: Storing) -> TASK_GEN: - """Put""" - yield UP.Wait(1.0) - yield UP.Put(actor.the_store, "Second") - - -f1 = UP.TaskNetworkFactory.from_single_terminating("GET", Getting) -f2 = UP.TaskNetworkFactory.from_single_terminating("PUT", Putting) - - -def _build_actor() -> Storing: - storing = Storing(name="example") - net = f1.make_network() - storing.add_task_network(net) - storing.start_network_loop(net.name, "Getting") - net = f2.make_network() - storing.add_task_network(net) - storing.start_network_loop(net.name, "Putting") - return storing - - -def test_cancel_return() -> None: - """For issue 110: https://github.com/gtri/upstage/issues/110 - - Demonstrate that a cancelled get request can return the item, - and that it does so with a Put, so other getters can have it. - """ - # Standard behavior should work. - with UP.EnvironmentContext() as env: - storing = _build_actor() - env.run() - assert env.now == 1 - assert storing.result == "Second" - - # Run until 0.5, then make an interrupt that happens at time 1 - # The Put request should have placed the item in the store, but - # the next processes shouldn't have gotten it. - with UP.EnvironmentContext() as env: - storing = _build_actor() - env.run(until=0.5) - - def _proc() -> SIMPY_GEN: - yield env.timeout(1.0 - env.now) - # One more zero time timeout to force putting the interrupt - # after the get request - yield env.timeout(0.0) - tasks = storing.get_running_tasks() - task_data = tasks["GET"] - task_data.process.interrupt(cause="Interrupted you") - - env.process(_proc()) - # Run until the timeouts are supposed to act. - env.run(until=1.0) - # The queue should have two items, both timeouts, and the shorter one is - # second. - assert len(env._queue) == 2 - assert env._queue[0][0] == 1.0 - assert env._queue[1][0] == 1.0 - assert isinstance(env._queue[0][-1], Timeout) - assert isinstance(env._queue[1][-1], Timeout) - assert env._queue[0][-1]._delay == 1.0 - assert env._queue[1][-1]._delay == 0.5 - - assert env.now == 1 - assert storing.result == "First" - # Now step through, making sure our expectation of ordering is true - assert len(storing.the_store.items) == 0 - env.step() - assert len(env._queue) == 2 - assert isinstance(env._queue[0][-1], Timeout) - assert isinstance(env._queue[1][-1], StorePut) - # The item has been put in the store, but the StorePut is waiting - # to signal the gets. - assert len(storing.the_store.items) == 1 - env.step() - assert len(env._queue) == 2 - assert isinstance(env._queue[0][-1], StorePut) - assert isinstance(env._queue[1][-1], Timeout) - # One step to get the StorePut done - env.step() - # Next step runs the timeout, which will queue the interrupt - # before StoreGet - env.step() - assert len(env._queue) == 4 - # The actor's store no longer has the item - assert len(storing.the_store.items) == 0 - # but the item isn't out yet. - assert storing.result == "First" - - # finish out the interrupts - env.run() - - # We need the store to have the item at the end of this - assert storing.the_store.items == ["Second"] - - -if __name__ == "__main__": - test_cancel_return() diff --git a/src/upstage_des/test/test_routines.py b/src/upstage_des/test/test_routines.py deleted file mode 100644 index a6e98cd..0000000 --- a/src/upstage_des/test/test_routines.py +++ /dev/null @@ -1,290 +0,0 @@ -from typing import Any - -import simpy as SIM - -import upstage_des.api as UP -from upstage_des.type_help import ROUTINE_GEN, SIMPY_GEN, TASK_GEN - - -class ExampleActor(UP.Actor): - value = UP.State[int](default=3) - - -class SimpleRoutine(UP.Routine): - def __init__(self, time: float) -> None: - self.time = time - - def run(self) -> ROUTINE_GEN: - yield UP.Wait(self.time, rehearsal_time_to_complete=self.time * 2) - - -class SomeTask(UP.Task): - def task(self, *, actor: ExampleActor) -> TASK_GEN: - self.set_marker("Routine") - yield SimpleRoutine(actor.value) - self.set_marker("Wait") - yield UP.Wait(2.0) - - def on_interrupt(self, *, actor: UP.Actor, cause: str) -> UP.InterruptStates: - self._v = self.get_marker() - return UP.InterruptStates.END - - -def test_simple_routine() -> None: - # The simple routine is to wait with a different rehearsal time. - # We run the task regularly and in rehearsal to check that the - # routine is passing back the right things. - - # Regular running - with UP.EnvironmentContext() as env: - act = ExampleActor(name="Example", value=3) - task = SomeTask() - task.run(actor=act) - env.run() - assert env.now == 5 - assert task.get_marker() == "Wait" - - # Rehearsing should take twice as long (and a 2 unit wait) - with UP.EnvironmentContext() as env: - act = ExampleActor(name="Example", value=3) - task = SomeTask() - new = task.rehearse(actor=act) - assert new.env.now == 8 - - -class CancelRoutine(UP.Routine): - def __init__(self, store: SIM.Store) -> None: - self.evt: UP.Get | None = None - self.store = store - self.result: Any = None - - def run(self) -> ROUTINE_GEN: - self.evt = UP.Get(self.store) - yield self.evt - self.result = [self.evt.get_value()] - - self.evt = UP.Get(self.store) - yield self.evt - self.result.append(self.evt.get_value()) - - def cancel(self) -> ROUTINE_GEN: - while self.result: - yield UP.Put(self.store, self.result.pop()) - - -class ExampleActor2(UP.Actor): - value = UP.State[list[str]](default_factory=list) - store = UP.ResourceState[SIM.Store](default=SIM.Store) - - -class CancelTask(UP.Task): - def task(self, *, actor: ExampleActor2) -> TASK_GEN: - routine = CancelRoutine(actor.store) - yield routine - assert routine.result is not None - actor.value = routine.result - - def on_interrupt(self, *, actor: ExampleActor2, cause: Any) -> UP.InterruptStates: - return UP.InterruptStates.END - - -def test_routine_cancel() -> None: - # Routines that cancel need to be able to clean up. - # First, check that the routine - - def _placer(store: SIM.Store, item: Any, t: float, env: SIM.Environment) -> SIMPY_GEN: - yield env.timeout(t) - yield store.put(item) - - # First check, does the routine do what we think? - with UP.EnvironmentContext() as env: - actor = ExampleActor2(name="example") - task = CancelTask() - proc = task.run(actor=actor) - place = _placer(actor.store, "first", 2.0, env) - env.process(place) - place = _placer(actor.store, "second", 2.5, env) - env.process(place) - env.run(until=3) - - assert actor.store.items == [] - assert actor.value == ["first", "second"] - - # Check cancelling partway - with UP.EnvironmentContext() as env: - actor = ExampleActor2(name="example") - task = CancelTask() - proc = task.run(actor=actor) - place = _placer(actor.store, "first", 2.0, env) - env.process(place) - env.run(until=3) - - proc.interrupt() - env.run() - # The item should get put back on cancel - assert actor.store.items == ["first"] - - # Check cancelling before anything - with UP.EnvironmentContext() as env: - actor = ExampleActor2(name="example") - task = CancelTask() - proc = task.run(actor=actor) - env.run(until=3) - - proc.interrupt() - env.run() - assert actor.value == [] - - # For fun and enrichment, see what rehearsal does - with UP.EnvironmentContext() as env: - actor = ExampleActor2(name="example") - task = CancelTask() - actor_clone = task.rehearse(actor=actor) - assert actor_clone.value == [UP.PLANNING_FACTOR_OBJECT] * 2 - - -class ExampleActor3(UP.Actor): - reset = UP.State[bool](default=True) - timeout = UP.State[float]() - store = UP.ResourceState[SIM.Store](default=SIM.Store) - - -class WindowedTask(UP.Task): - def task(self, *, actor: ExampleActor3) -> TASK_GEN: - routine = UP.WindowedGet( - store=actor.store, - timeout=actor.timeout, - reset_window=actor.reset, - get_kwargs=dict(rehearsal_time_to_complete=2.2), - ) - answer = yield routine - self.set_actor_knowledge(actor, "result", answer) - - def on_interrupt(self, *, actor: ExampleActor3, cause: str) -> UP.InterruptStates: - if cause == "end": - return UP.InterruptStates.END - elif cause == "ignore": - return UP.InterruptStates.IGNORE - elif cause == "restart": - return UP.InterruptStates.RESTART - else: - raise UP.SimulationError("Bad cause") - - -def _placer(env: SIM.Environment, act: ExampleActor3) -> SIMPY_GEN: - yield env.timeout(1.0) - yield act.store.put("1") - yield env.timeout(3.0) - yield act.store.put("2") - yield env.timeout(4.0) - yield act.store.put("3") - - -def test_windowed_get() -> None: - # Test the windowed get w/ window reset on - with UP.EnvironmentContext() as env: - act = ExampleActor3( - name="example", - reset=True, - timeout=5.0, - ) - - task = WindowedTask() - task.run(actor=act) - - env.process(_placer(env, act)) - env.run() - - assert act._knowledge["result"] == ["1", "2", "3"] - - # Same test, but no reset. We shouldn't get the 3rd item. - with UP.EnvironmentContext() as env: - act = ExampleActor3( - name="example", - reset=False, - timeout=5.0, - ) - - task = WindowedTask() - task.run(actor=act) - - env.process(_placer(env, act)) - env.run() - - assert act._knowledge["result"] == ["1", "2"] - - # An interrupt in the task process will cancel the routine - # and everything will be back in the store. - with UP.EnvironmentContext() as env: - act = ExampleActor3( - name="example", - reset=True, - timeout=5.0, - ) - - task = WindowedTask() - proc = task.run(actor=act) - - env.process(_placer(env, act)) - env.run(until=5.0) - proc.interrupt(cause="end") - env.run() - - assert "result" not in act._knowledge - assert act.store.items == ["1", "2", "3"] - - # Interrupt with IGNORE and see the result as before. - with UP.EnvironmentContext() as env: - act = ExampleActor3( - name="example", - reset=True, - timeout=5.0, - ) - - task = WindowedTask() - proc = task.run(actor=act) - - env.process(_placer(env, act)) - env.run(until=5.0) - proc.interrupt(cause="ignore") - env.run() - - assert act._knowledge["result"] == ["1", "2", "3"] - - # Interrupt with RESTART and modify the timeout. The request will be redone - # but won't last long enough to get the 3rd item. - with UP.EnvironmentContext() as env: - act = ExampleActor3( - name="example", - reset=False, - timeout=5.0, - ) - - task = WindowedTask() - proc = task.run(actor=act) - - env.process(_placer(env, act)) - env.run(until=5.0) - act.timeout = 1 - proc.interrupt(cause="restart") - env.run() - - assert act._knowledge["result"] == ["1", "2"] - assert act.store.items == ["3"] - - # Rehearsal - with UP.EnvironmentContext() as env: - act = ExampleActor3( - name="example", - reset=False, - timeout=5.0, - ) - - task = WindowedTask() - new = task.rehearse(actor=act) - assert new._knowledge["result"] == [UP.PLANNING_FACTOR_OBJECT] - assert new.env.now == 2.2 - - -if __name__ == "__main__": - test_windowed_get() diff --git a/src/upstage_des/test/test_sim_wide_tracking.py b/src/upstage_des/test/test_sim_wide_tracking.py deleted file mode 100644 index 55d1142..0000000 --- a/src/upstage_des/test/test_sim_wide_tracking.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import upstage_des.api as UP - - -class Example(UP.Actor): - a_value = UP.State[float](default=1.0) - - -class Example2(Example): - b_value = UP.State[float](default=2.0) - - -class EnvHolder(UP.UpstageBase): ... - - -def test_actor_tracking() -> None: - with UP.EnvironmentContext(): - env = EnvHolder() - actor = Example(name="Example_1") - - actor2 = Example2( - name="Example_2", - b_value=3.0, - ) - - actors = env.get_actors() - assert len(actors) == 2, "Wrong number of actors stored" - assert actor in actors - assert actor2 in actors - - actor_entities = env.get_entity_group("Example") - assert actor in actor_entities - assert len(actor_entities) == 2 - - actor_entities = env.get_entity_group("Example2") - assert actor2 in actor_entities - assert len(actor_entities) == 1 - - -def test_env_reset() -> None: - with UP.EnvironmentContext(): - env = EnvHolder() - actor = Example(name="Example_1") - - actors = env.get_actors() - assert len(actors) == 1, "Wrong number of actors stored" - assert actor in actors - - with UP.EnvironmentContext(): - env = EnvHolder() - actors = env.get_actors() - assert actor not in actors - - actors = env.get_actors() - assert actor not in actors - - -def test_actor_multi_tracking() -> None: - class Person(UP.Actor, entity_groups=("A Person", "VIP")): - pass - - with UP.EnvironmentContext(): - env = EnvHolder() - p = Person(name="some person") - people = env.get_entity_group("A Person") - assert len(people) == 1 - vips = env.get_entity_group("VIP") - assert len(vips) == 1 - actors = env.get_actors() - assert len(actors) == 1 - assert p in actors - assert len(env.get_all_entity_groups()) == 3 - - class Doctor(UP.Actor, entity_groups=("Hospital Worker")): - pass - - d = Doctor(name="Doc Oc") - assert len(env.get_all_entity_groups()) == 5 - assert d not in env.get_entity_group("Person") - - -class Sensor(UP.NamedUpstageEntity): - def __init__(self, name: str, radius: float) -> None: - super().__init__() - self.name = name - self.radius = radius - - -class RadarSensor(Sensor, entity_groups="RADAR"): - pass - - -class LightSensor(Sensor): - pass - - -class NextRadar(RadarSensor): - pass - - -def test_entity_tracking() -> None: - with UP.EnvironmentContext(): - env = EnvHolder() - simple_sensor = Sensor(name="Sense1", radius=3) - radar_sensor = RadarSensor(name="A Radar", radius=10) - light_sensor = LightSensor(name="A Light", radius=4.5) - - sensors = env.get_entity_group("Sensor") - radars = env.get_entity_group("RADAR") - other_radars = env.get_entity_group("RadarSensor") - lights = env.get_entity_group("LightSensor") - - assert len(sensors) == 3 - assert simple_sensor in sensors - assert len(radars) == 1 - assert radar_sensor in radars - assert len(lights) == 1 - assert light_sensor in lights - assert len(other_radars) == 1 - - assert len(env.get_all_entity_groups()) == 4 - - # Check that we do inherit subclass and the defined ones. - nxt = NextRadar(name="More testing", radius=6.45) - assert nxt in env.get_entity_group("RADAR") - assert nxt in env.get_entity_group("Sensor") - assert nxt in env.get_entity_group("RadarSensor") - - -def test_multi_tracking() -> None: - with UP.EnvironmentContext(): - env = EnvHolder() - - class LightSensor(RadarSensor, entity_groups=("MySensor", "Light")): - pass - - LightSensor(name="A Light", radius=4.5) - lights = env.get_entity_group("Light") - sensors = env.get_entity_group("MySensor") - assert len(lights) == 1 - assert len(sensors) == 1 - - -def test_env_reset_tracking() -> None: - with UP.EnvironmentContext(): - env = EnvHolder() - simple_sensor = Sensor(name="Sense1", radius=3) - sensors = env.get_entity_group("Sensor") - assert len(sensors) == 1 - assert simple_sensor in sensors - - with UP.EnvironmentContext(): - env = EnvHolder() - sensors = env.get_entity_group("Sensor") - assert simple_sensor not in sensors - - sensors = env.get_entity_group("Sensor") - assert simple_sensor not in sensors - assert len(env.get_all_entity_groups()) == 0 - - -def test_multi_inheritence_tracking() -> None: - """Test a bug found in entity group tracking. - - The bug was that multiple inheritance wasn't moving all the entity group - names to the final instance. - - This also tests that mixin classes - """ - - class Thing(UP.Actor, entity_groups=["Here"]): ... - - class Other(UP.Actor, entity_groups=["Missing"]): ... - - class MethodMixin: - def a_method(self) -> int: - return 3 - - class Mixed(Thing, Other, MethodMixin): ... - - with UP.EnvironmentContext(): - m = Mixed(name="A") - grps = m.get_all_entity_groups() - for key in ["Mixed", "Thing", "Other", "Here", "Missing"]: - assert key in grps - assert grps[key] == [m] - assert m.a_method() == 3 - - -if __name__ == "__main__": - test_multi_inheritence_tracking() diff --git a/src/upstage_des/test/test_stage.py b/src/upstage_des/test/test_stage.py deleted file mode 100644 index 462a451..0000000 --- a/src/upstage_des/test/test_stage.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.api import ( - EnvironmentContext, - UpstageBase, - UpstageError, - add_stage_variable, - get_stage, - get_stage_variable, -) -from upstage_des.base import clear_top_context, create_top_context - - -class Stager(UpstageBase): ... - - -def test_stage() -> None: - with EnvironmentContext(): - source = Stager() - # Complain when accessing an unset attribute - with pytest.raises(AttributeError): - source.stage.stage_model - - # setting with the method - add_stage_variable("stage_model", 1) - assert source.stage.stage_model == 1 - - # Setting without the method - add_stage_variable("altitude_units", 2) - assert source.stage.altitude_units == 2 - - # Setting should yell after a set - with pytest.raises(UpstageError): - add_stage_variable("altitude_units", 3) - - # After the context, it should not exists - with pytest.raises(UpstageError): - source.stage - - -def test_contextless_stage() -> None: - ctx = create_top_context() - add_stage_variable("example", 1.234) - - assert get_stage_variable("example") == 1.234 - - stage = get_stage() - assert stage.example == 1.234 - - # dropping into a new context ignores the above - with EnvironmentContext(): - add_stage_variable("example", 8.675) - assert get_stage_variable("example") == 8.675 - - assert get_stage_variable("example") == 1.234 - - clear_top_context(ctx) - - with pytest.raises(ValueError, match="Stage should have been set."): - get_stage_variable("example") - - -if __name__ == "__main__": - test_contextless_stage() diff --git a/src/upstage_des/test/test_state.py b/src/upstage_des/test/test_state.py deleted file mode 100644 index 6d62111..0000000 --- a/src/upstage_des/test/test_state.py +++ /dev/null @@ -1,794 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from dataclasses import dataclass, fields -from typing import Any, cast - -import pytest -from simpy import Container, Environment, Store - -import upstage_des.api as UP -import upstage_des.resources.monitoring as monitor -from upstage_des.actor import Actor -from upstage_des.api import EnvironmentContext, SimulationError, UpstageError -from upstage_des.states import LinearChangingState, ResourceState, State -from upstage_des.task import TASK_GEN -from upstage_des.type_help import SIMPY_GEN - - -class StateTest: - state_one = State[Any]() - state_two = State[Any](recording=True) - - def __init__(self, env: Environment | None) -> None: - cast(UP.Actor, self) - self.env = env - # including for compatibility - self._mimic_states: dict[str, Any] = {} - self._state_listener = None - self._state_histories: dict[str, list[tuple[float, Any]]] = {} - - def set_one(self, val: Any) -> None: - self.state_one = val # type: ignore [arg-type] - - def set_two(self, val: Any) -> None: - self.state_two = val # type: ignore [arg-type] - - -class StateTestActor(Actor): - state_one = State[Any]() - state_two = State[Any](recording=True) - state_three = LinearChangingState(recording=True) - - -class MutableDefaultActor(Actor): - lister = State[list](default_factory=list) - diction = State[dict](default_factory=dict) - setstate = State[set](default_factory=set) - - -def test_state_fails_without_env() -> None: - """Test that recording states need the class to have an env attribute""" - tester = StateTest(None) - # the first one should not raise an error - tester.set_one(1) - - with pytest.raises(SimulationError): - tester.set_two(1) - - -def test_state_values() -> None: - """Test that we get the right state values we input""" - with EnvironmentContext(initial_time=1.5) as env: - tester = StateTest(env) - tester.state_one = 1 # type: ignore [arg-type] - assert tester.state_one == 1 # type: ignore [arg-type] - tester.state_two = 2 # type: ignore [arg-type] - assert tester.state_two == 2 # type: ignore [arg-type] - - -def test_state_recording() -> None: - with EnvironmentContext(initial_time=1.5) as env: - tester = StateTest(env) - tester.state_two = 2 # type: ignore [arg-type] - assert "state_two" in tester._state_histories - env.run(until=2.5) - tester.state_two = 3 # type: ignore [arg-type] - assert len(tester._state_histories["state_two"]) == 2 - assert tester._state_histories["state_two"][0] == (1.5, 2) - assert tester._state_histories["state_two"][1] == (2.5, 3) - - -def test_state_mutable_default() -> None: - with EnvironmentContext(initial_time=1.5): - tester = MutableDefaultActor(name="Example") - tester2 = MutableDefaultActor(name="Example2") - assert id(tester.lister) != id(tester2.lister) - tester.lister.append(1) - assert len(tester2.lister) == 0 - assert len(tester.lister) == 1 - - assert id(tester.diction) != id(tester2.diction) - tester2.diction[1] = 2 - assert len(tester.diction) == 0 - assert len(tester2.diction) == 1 - - assert id(tester.setstate) != id(tester2.setstate) - tester2.setstate.add(1) - assert len(tester.setstate) == 0 - assert len(tester2.setstate) == 1 - - -def test_state_values_from_init() -> None: - with EnvironmentContext() as env: - tester = StateTestActor( - name="testing", - state_one=1, - state_two=2, - state_three=4, - ) - env.run(until=1.5) - assert tester.state_one == 1 - assert tester.state_two == 2 - assert tester.state_three == 4 - tester.state_three = 3 - assert "state_three" in tester._state_histories - assert tester._state_histories["state_three"] == [(0.0, 4), (1.5, 3)] - - -def test_state_none_allowed() -> None: - class NoneActor(Actor): - example = State[int](allow_none_default=True) - - with EnvironmentContext(): - ex = NoneActor(name="Example") - - with pytest.raises(SimulationError, match="State example should have been set"): - ex.example - - ex.example = 3 - assert ex.example == 3 - - -def test_linear_changing_state() -> None: - state_three_init = 3 - init_time = 1.5 - rate = 3.1 - timestep = 1 - - with EnvironmentContext(initial_time=init_time) as env: - tester = StateTestActor( - name="testing", - state_one=1, - state_two=2, - state_three=state_three_init, - ) - - task = UP.Task() - - tester.activate_state(state="state_three", task=task, rate=rate) - assert "state_three" in tester._active_states - assert tester._active_states["state_three"] == tester.get_active_state_data( - "state_three", without_update=True - ) - env.run(until=init_time + timestep) - # Test getting the value before it's ended - assert tester.state_three == rate * timestep + state_three_init - env.run(until=init_time + timestep * 2) - state_data = tester.get_active_state_data("state_three", without_update=True) - assert state_data["started_at"] == timestep + init_time - assert state_data["rate"] == rate - tester.deactivate_state(state="state_three", task=task) - assert "state_three" not in tester._active_states - assert tester.state_three == rate * timestep * 2 + state_three_init - - -def test_resource_state_valid_types() -> None: - class Holder(Actor): - res = ResourceState[Store](valid_types=Store) - - with EnvironmentContext(): - Holder( - name="example", - res={"kind": Store}, - ) - - with pytest.raises(UpstageError): - Holder( - name="example", - res={"kind": Container}, - ) - - with pytest.raises(UpstageError): - - class _(Actor): - res = ResourceState[Store](valid_types=(1,)) # type: ignore [arg-type] - - with pytest.raises(UpstageError): - - class _(Actor): # type: ignore [no-redef] - res = ResourceState[Store](valid_types=(Actor,)) - - -def test_resource_state_set_protection() -> None: - class Holder(Actor): - res = ResourceState[Store](valid_types=(Store)) - - with EnvironmentContext(): - h = Holder( - name="example", - res={"kind": Store}, - ) - with pytest.raises(UpstageError, match=".+It cannot be changed once set.+"): - h.res = 1.0 - - -def test_resource_state_no_default_init() -> None: - class Holder(Actor): - res = ResourceState[Store]() - - with EnvironmentContext(): - with pytest.raises(UpstageError, match="Missing values for states"): - h = Holder( - name="example", - ) - - with pytest.raises(UpstageError, match="No resource type"): - h = Holder( - name="example", - res={}, - ) - - h = Holder( - name="example", - res={"kind": Store}, - ) - assert isinstance(h.res, Store) - - -def test_resource_state_default_init() -> None: - class Holder(Actor): - res = ResourceState[Store](default=Store) - res2 = ResourceState[Container]( - default=Container, default_kwargs={"capacity": 11, "init": 5} - ) - - class HolderBad(Actor): - res = ResourceState[Store](default=Store) - res2 = ResourceState[Container](default=Container, default_kwargs={"capa": 11, "init": 5}) - - with EnvironmentContext(): - h = Holder(name="Example") - assert isinstance(h.res, Store) - assert h.res2.capacity == 11 - assert h.res2.level == 5 - - h = Holder(name="Example", res={"capacity": 10}, res2={"capacity": 12}) - assert isinstance(h.res, Store) - assert h.res.capacity == 10 - assert h.res2.capacity == 12 - assert h.res2.level == 5 - - with pytest.raises(UpstageError): - HolderBad(name="Bad one") - - -def test_resource_state_kind_init() -> None: - class Holder(Actor): - res = ResourceState[Store]() - - with EnvironmentContext(): - h = Holder(name="Example", res={"kind": Store, "capacity": 10}) - assert isinstance(h.res, Store) - assert h.res.capacity == 10 - - h = Holder(name="Example", res={"kind": Container, "capacity": 100, "init": 50}) - assert isinstance(h.res, Container) - assert h.res.capacity == 100 - assert h.res.level == 50 - - test_resources = [ - x - for x in monitor.__dict__.values() - if isinstance(x, type) and issubclass(x, Store | Container) - ] - for the_class in test_resources: - h = Holder(name="Example", res={"kind": the_class, "capacity": 99}) - assert isinstance(h.res, the_class) - assert h.res.capacity == 99 - - -def test_resource_state_simpy_store_running() -> None: - class Holder(Actor): - res = ResourceState[Store]() - - with EnvironmentContext() as env: - h = Holder(name="Example", res={"kind": Store, "capacity": 10}) - - def put_process(entity: Holder) -> SIMPY_GEN: - for i in range(11): - yield env.timeout(1.0) - yield entity.res.put(f"Item {i}") - return "Done" - - def get_process(entity: Holder) -> SIMPY_GEN: - res = yield entity.res.get() - return res - - proc_1 = env.process(put_process(h)) - proc_2 = env.process(get_process(h)) - env.run() - assert proc_2.value == "Item 0" - assert proc_1.value == "Done" - - -def test_resource_clone() -> None: - class Holder(Actor): - res = ResourceState[Store](default=Store) - - class Holder2(Actor): - res = ResourceState[Container](default=Container) - - with EnvironmentContext(): - holder = Holder(name="example") - holder_2 = holder.clone() - assert id(holder_2.res.items) != id(holder.res.items) - - holder = Holder2(name="example") - holder_2 = holder.clone() - assert id(holder_2.res.level) != id(holder.res.level) - - -class HelperCallback: - def __init__(self) -> None: - self.cbacks: list[tuple[Any, Any]] = [] - - def _callbacker(self, instance: Any, value: Any) -> None: - self.cbacks.append((instance, value)) - - -def test_state_callback() -> None: - class CbackActor(Actor): - state_one = State[Any](recording=True) - - helper = HelperCallback() - with EnvironmentContext(): - actor = CbackActor( - name="Test", - state_one=1, - ) - - actor._add_callback_to_state("source", helper._callbacker, "state_one") - actor.state_one = 2 - assert len(helper.cbacks) == 1 - assert helper.cbacks[0][1] == 2 - actor._remove_callback_from_state("source", "state_one") - - actor.state_one = 3 - assert len(helper.cbacks) == 1 - - -def test_matching_states() -> None: - """Test the state matching code. - At this time, state matching only works with CommunicationStore. It's the - only state with a special attribute attached to it. - """ - - class Worker(UP.Actor): - sleepiness = UP.State[float](default=0.0, valid_types=(float,)) - walkie = UP.CommunicationStore(modes=["UHF", "loudspeaker"]) - intercom = UP.CommunicationStore(modes="loudspeaker") - - with EnvironmentContext(): - worker = Worker(name="Billy") - store_name = worker._get_matching_state( - UP.CommunicationStore, - {"_modes": "loudspeaker"}, - ) - assert store_name is not None - store = getattr(worker, store_name, "") - assert store is worker.intercom, "Wrong state retrieved" - assert store is not worker.walkie, "Wrong state retrieved" - assert getattr(worker, "_intercom__mode_names_") == set(["loudspeaker"]) - assert getattr(worker, "_walkie__mode_names_") == set(["UHF", "loudspeaker"]) - - # Show the FCFS behavior - state_name = worker._get_matching_state( - UP.State, - ) - assert state_name is not None - value = getattr(worker, state_name) - assert value == worker.sleepiness, "Wrong state retrieved" - - # Show the FCFS behavior with state type - state_name = worker._get_matching_state( - UP.CommunicationStore, - ) - assert state_name is not None - value = getattr(worker, state_name) - assert value is worker.walkie, "Wrong state retrieved" - - -def test_dictionary_state() -> None: - """Test multi states.""" - - class TheActor(UP.Actor): - holder = UP.DictionaryState[int | str](valid_types=(int, str), recording=True) - - use = {"item": 0, "other": 4, "new": "A"} - - with UP.EnvironmentContext() as env: - ta = TheActor(name="example", holder=use) - ans = {k: v for k, v in ta.holder.items()} - assert ans == use - - for k, v in use.items(): - assert ta.holder[k] == v - - # Making sure we are not using the original object - use["newer"] = 4 - assert "newer" not in ta.holder - - ta.holder["newer"] = 5 - env.run(3) - ta.holder["new"] = 1 - with pytest.raises( - TypeError, - match="Bad type for dictionary", - ): - ta.holder["item"] = 2.0 # type: ignore [assignment] - ta.holder["item"] = 42 - res = ta.holder.setdefault("newest", "a value") - assert res == "a value" - res = ta.holder.setdefault("item", "xyz") - # Since the error is skipped, it's still set as 2.0 - assert res == 42 - - ks = ["item", "other", "new", "newer", "newest"] - vs = [42, 4, 1, 5, "a value"] - assert list(ta.holder.keys()) == ks - assert list(ta.holder.values()) == vs - assert list(ta.holder.items()) == [(k, v) for k, v in zip(ks, vs)] - assert ta.holder == {k: v for k, v in zip(ks, vs)} - - env.run(5) - ta.holder["item"] = 10 - ta.holder["other"] = 11 - - for key in ta.holder.keys(): - rec_key = f"holder.{key}" - assert rec_key in ta._state_histories - for key in ta.holder: - assert key in ks - assert ta._state_histories["holder.item"] == [(0.0, 0), (3.0, 42), (5.0, 10)] - assert ta._state_histories["holder.other"] == [(0.0, 4), (5.0, 11)] - assert ta._state_histories["holder.new"] == [(0.0, "A"), (3.0, 1)] - assert ta._state_histories["holder.newer"] == [(0.0, 5)] - assert ta._state_histories["holder.newest"] == [(3.0, "a value")] - - -def test_dictionary_state_record() -> None: - def total_recorder(time: float, value: dict[str, int]) -> int: - return sum(value.values()) - - class Usher(UP.Actor): - people_seen = UP.DictionaryState[int]( - valid_types=int, - recording=True, - recording_functions=[(total_recorder, "total_customers")], - ) - rando = UP.DictionaryState[Any](recording=True) - - class TicketTaking(UP.Task): - def task(self, *, actor: Usher) -> TASK_GEN: - for customer in ["adult", "adult", "child", "adult", "child", "vip"]: - actor.people_seen.setdefault(customer, 0) - actor.people_seen[customer] += 1 - yield UP.Wait(0.1) - - with UP.EnvironmentContext() as env: - ush = Usher(name="Yeah", people_seen={"adult": 0, "child": 0}, rando={"stuff": {}}) - task = TicketTaking() - task.run(actor=ush) - env.run() - ush.rando["stuff"]["here"] = 1 - assert env.now == 0.6 - assert "people_seen.adult" in ush._state_histories - assert "people_seen.child" in ush._state_histories - assert "people_seen.vip" in ush._state_histories - assert "total_customers" in ush._state_histories - - -def test_dataclass_state() -> None: - @dataclass - class TestDC: - a: int - b: float - - def recorder(time: float, value: TestDC) -> float: - return value.a + value.b - - class ExampleActor(UP.Actor): - dc_state = UP.DataclassState[TestDC]( - valid_types=TestDC, - recording=True, - recording_functions=[(recorder, "total_of_data")], - ) - - class SomeTask(UP.Task): - def task(self, *, actor: ExampleActor) -> TASK_GEN: - actor.dc_state.a += 1 - actor.dc_state.b += 4 - yield UP.Wait(0.1) - actor.dc_state.b += 4 - yield UP.Wait(0.1) - actor.dc_state.a -= 3 - - with UP.EnvironmentContext() as env: - ea = ExampleActor(name="Exam", dc_state=TestDC(0, 0.0)) - task = SomeTask() - task.run(actor=ea) - env.run() - assert env.now == 0.2 - # does fields work? - fs = fields(ea.dc_state) - assert [f.name for f in fs] == ["a", "b"] - - with pytest.raises(TypeError, match="doesn't match type:"): - ea.dc_state.a = "cause error" # type: ignore [assignment] - - # let's check histories - assert len(ea._state_histories) == 3 - assert ea._state_histories["dc_state.a"] == [(0.0, 0), (0.0, 1), (0.2, -2)] - assert ea._state_histories["dc_state.b"] == [(0.0, 0.0), (0.0, 4.0), (0.1, 8.0)] - assert ea._state_histories["total_of_data"] == [ - (0.0, 0.0), - (0.0, 1.0), - (0.0, 5.0), - (0.1, 9.0), - (0.2, 6.0), - ] - - assert ea.dc_state == TestDC(-2, 8.0) - - -def test_multistore_state() -> None: - class MSActor(UP.Actor): - mstate = UP.MultiStoreState[UP.SelfMonitoringStore]( - valid_types=Store, - default=UP.SelfMonitoringStore, - default_kwargs={"capacity": 3}, - ) - cstate = UP.MultiStoreState[Container]( - valid_types=Container, - default=Container, - ) - - with UP.EnvironmentContext() as env: - ms = MSActor( - name="Example", - mstate=["One", "Two"], - cstate={"New": {"kind": UP.SelfMonitoringContainer, "init": 100}}, - ) - mstate = ms.mstate - assert len(mstate) == 2 - assert "One" in mstate - assert "Two" in mstate - assert ms.mstate["One"].capacity == 3 - assert ms.mstate["Two"].capacity == 3 - assert ms.cstate["New"].level == 100 - - def _proc() -> SIMPY_GEN: - yield ms.mstate["One"].put(1) - yield ms.mstate["Two"].put(2) - yield env.timeout(1.0) - yield ms.cstate["New"].put(20) - - env.process(_proc()) - env.run() - assert ms.mstate["One"].items == [1] - assert ms.mstate["Two"].items == [2] - assert ms.cstate["New"].level == 120 - assert hasattr(ms.cstate["New"], "_quantities") - assert ms.cstate["New"]._quantities == [(0, 100), (1, 120)] - - with pytest.raises(UpstageError, match="is of type SIMPY_GEN: - yield UP.Put(wh.storage["bucket"], 20).as_event() - - env.process(_proc()) - env.run() - assert wh.storage["bucket"].level == 50 - assert hasattr(wh.storage["bucket"], "_quantities") - assert wh.storage["bucket"]._quantities == [(0.0, 30), (2.0, 50)] - - -def test_extra_recording() -> None: - """Test that the extra recording works.""" - - def recorder(time: float, value: float) -> float: - return time * value - - def recorder2(time: float, value: float) -> float: - return time * (value + 1) - - class FailingRecord(UP.Actor): - a_state = UP.State[float]( - default=0.0, - recording_functions=[(recorder, "time_mult")], - ) - b_state = UP.State[float]( - default=0.0, - recording_functions=[(recorder, "time_mult")], - ) - - class FailingRecord2(UP.Actor): - a_state = UP.State[float]( - default=0.0, - recording_functions=[(recorder, "time_mult")], - ) - b_state = UP.State[float]( - default=0.0, - recording_functions=[(recorder, "a_state")], - ) - - class RecordingStates(UP.Actor): - a_state = UP.State[float]( - default=0.0, - recording=True, - recording_functions=[(recorder, "a_mult")], - ) - b_state = UP.State[float]( - default=0.0, - recording=True, - recording_functions=[ - (recorder, "b_mult"), - (recorder2, "b_mult2"), - ], - ) - - with UP.EnvironmentContext() as env: - with pytest.raises(SimulationError, match="Duplicated state or recording name"): - FailingRecord(name="example") - - with pytest.raises(SimulationError, match="Duplicated state or recording name"): - FailingRecord2(name="example") - - rs = RecordingStates(name="example") - env.run(until=1) - rs.a_state = 3 - rs.b_state = 4 - env.run(until=3) - rs.a_state += 1 - rs.b_state += 1 - assert "a_state" in rs._state_histories - assert "a_mult" in rs._state_histories - assert "b_mult" in rs._state_histories - assert "b_mult2" in rs._state_histories - assert "b_state" in rs._state_histories - assert len(rs._state_histories) == 5 - - assert rs._state_histories["a_state"] == [(0.0, 0), (1.0, 3), (3.0, 4)] - assert rs._state_histories["a_mult"] == [(0.0, 0), (1.0, 3), (3.0, 12)] - assert rs._state_histories["b_state"] == [(0.0, 0), (1.0, 4), (3.0, 5)] - assert rs._state_histories["b_mult"] == [(0.0, 0), (1.0, 4), (3.0, 15)] - assert rs._state_histories["b_mult2"] == [(0.0, 0), (1.0, 5), (3.0, 18)] - - -def test_extra_recording_docs() -> None: - """Test the recording example from the docs. - - This also checks the typing and loading from a class. - """ - - from collections import Counter - - class NameStorage: - def __init__(self) -> None: - self.seen: dict[str, int] = Counter() - self.seen[""] = 0 - - def __call__(self, time: float, value: str) -> float: - if value: - self.seen[value] += 1 - return max(self.seen.values()) - - def first_letter(time: float, value: str) -> str: - if value: - return value[0] - return "" - - class Cashier(UP.Actor): - people_seen = UP.State[str]( - default="", - recording=True, - record_duplicates=True, - recording_functions=[ - (NameStorage, "max_repeats"), - (first_letter, "first_letter"), - ], - ) - - with UP.EnvironmentContext(): - cash = Cashier(name="Ertha") - cash2 = Cashier(name="Bertha") - cash.people_seen = "James" - cash.people_seen = "Bob" - cash.people_seen = "James" - cash.people_seen = "Fred" - cash.people_seen = "James" - - assert cash._state_histories["max_repeats"] == [ - (0.0, 0), - (0.0, 1), - (0.0, 1), - (0.0, 2), - (0.0, 2), - (0.0, 3), - ] - assert cash._state_histories["first_letter"] == [ - (0.0, ""), - (0.0, "J"), - (0.0, "B"), - (0.0, "J"), - (0.0, "F"), - (0.0, "J"), - ] - - assert cash2._state_histories["max_repeats"] == [(0.0, 0)] - - class CashierNonDup(UP.Actor): - people_seen = UP.State[str]( - default="", - recording=True, - record_duplicates=False, - recording_functions=[ - (NameStorage, "max_repeats"), - (first_letter, "first_letter"), - ], - ) - - with UP.EnvironmentContext(): - cash3 = CashierNonDup(name="Ertha") - cash3.people_seen = "James" - cash3.people_seen = "Bob" - cash3.people_seen = "James" - cash3.people_seen = "Fred" - cash3.people_seen = "James" - - assert cash3._state_histories["max_repeats"] == [ - (0.0, 0), - (0.0, 1), - (0.0, 2), - (0.0, 3), - ] - assert cash3._state_histories["first_letter"] == [ - (0.0, ""), - (0.0, "J"), - (0.0, "B"), - (0.0, "J"), - (0.0, "F"), - (0.0, "J"), - ] - - -if __name__ == "__main__": - test_multistore_state() diff --git a/src/upstage_des/test/test_state_and_task_sharing.py b/src/upstage_des/test/test_state_and_task_sharing.py deleted file mode 100644 index 52fa2c2..0000000 --- a/src/upstage_des/test/test_state_and_task_sharing.py +++ /dev/null @@ -1,247 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Any - -import pytest - -from upstage_des.api import ( - Actor, - CartesianLocationChangingState, - EnvironmentContext, - GeodeticLocationChangingState, - InterruptStates, - LinearChangingState, - State, - Task, - Wait, - add_stage_variable, -) -from upstage_des.data_types import CartesianLocation, GeodeticLocation -from upstage_des.geography import Spherical -from upstage_des.type_help import TASK_GEN - - -class Mover(Actor): - location = CartesianLocationChangingState(recording=True) - speed = State[float](recording=True) - fuel = LinearChangingState(recording=True) - fuel_burn = State[float](recording=True) - - def get_distance(self, waypoints: list[CartesianLocation]) -> float: - d = waypoints[0] - self.location - for i in range(1, len(waypoints)): - d += waypoints[i] - waypoints[i - 1] - return d - - -class MoverGeo(Actor): - location = GeodeticLocationChangingState(recording=True) - speed = State[float](recording=True) - fuel = LinearChangingState(recording=True) - fuel_burn = State[float](recording=True) - - def get_distance(self, waypoints: list[GeodeticLocation]) -> float: - d = waypoints[0] - self.location - for i in range(1, len(waypoints)): - d += waypoints[i] - waypoints[i - 1] - return d - - -class MoveTask(Task): - def task(self, *, actor: Mover | MoverGeo) -> TASK_GEN: - destinations = list(self.get_actor_knowledge(actor, "destinations")) - actor.activate_state( - state="location", - task=self, - speed=actor.speed, - waypoints=destinations, - ) - actor.activate_state( - state="fuel", - task=self, - rate=actor.fuel_burn, - ) - dist = actor.get_distance(destinations) - time = dist / actor.speed - yield Wait(time) - actor.deactivate_all_states(task=self) - - def on_interrupt(self, *, actor: Mover | MoverGeo, cause: Any) -> InterruptStates: - if cause == "restart": - rem_wypts = actor.get_remaining_waypoints( - location_state="location", - ) - self.set_actor_knowledge( - actor, - "destinations", - rem_wypts, - overwrite=True, - ) - actor.speed /= 2 - return self.INTERRUPT.RESTART - else: - return self.INTERRUPT.END - - -WAYPOINTS = [ - (3, 4), - (6, 0), -] - -ATLANTA = [33.7490, -84.3880, 1050] -DENVER = [39.7392, -104.9903, 30_000] -SAN_FRAN = [37.7749, -122.4194, 0] - -WAYPOINTS_GEO = [ - DENVER, - SAN_FRAN, -] - - -def test_regular_run() -> None: - with EnvironmentContext() as env: - add_stage_variable("distance_units", "nmi") - add_stage_variable("altitude_units", "ft") - add_stage_variable("stage_model", Spherical) - - mover = Mover( - name="example", - location=CartesianLocation(0, 0), - speed=10, - fuel=500, - fuel_burn=10, - ) - waypoints = [CartesianLocation(*x) for x in WAYPOINTS] - mover.set_knowledge("destinations", waypoints) - task = MoveTask() - task.run(actor=mover) - env.run() - assert env.now == 1 - d = mover.location - waypoints[-1] - assert pytest.approx(d) == 0 - - -def test_first_interrupt() -> None: - with EnvironmentContext() as env: - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - add_stage_variable("stage_model", Spherical) - - mover = Mover( - name="example", - location=CartesianLocation(0, 0), - speed=10, - fuel=500, - fuel_burn=10, - ) - waypoints = [CartesianLocation(*x) for x in WAYPOINTS] - mover.set_knowledge("destinations", waypoints) - task = MoveTask() - proc = task.run(actor=mover) - env.run(until=0.25) - proc.interrupt(cause="restart") - env.run() - assert env.now == (2 - 0.25) - d = mover.location - waypoints[-1] - assert pytest.approx(d) == 0 - - -def test_second_interrupt() -> None: - with EnvironmentContext() as env: - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - add_stage_variable("stage_model", Spherical) - - mover = Mover( - name="example", - location=CartesianLocation(0, 0), - speed=10, - fuel=500, - fuel_burn=10, - ) - waypoints = [CartesianLocation(*x) for x in WAYPOINTS] - mover.set_knowledge("destinations", waypoints) - task = MoveTask() - proc = task.run(actor=mover) - env.run(until=0.75) - proc.interrupt(cause="restart") - env.run() - assert env.now == (2 - 0.75) - d = mover.location - waypoints[-1] - assert pytest.approx(d) == 0 - - -def test_regular_run_geo() -> None: - with EnvironmentContext() as env: - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - add_stage_variable("stage_model", Spherical) - - mover = MoverGeo( - name="example", - location=GeodeticLocation(*SAN_FRAN), - speed=10, - fuel=500000, - fuel_burn=10, - ) - waypoints = [GeodeticLocation(*x) for x in WAYPOINTS_GEO] - mover.set_knowledge("destinations", waypoints) - task = MoveTask() - task.run(actor=mover) - env.run() - assert env.now == 164.81767330428073 - d = mover.location - waypoints[-1] - assert pytest.approx(d) == 0 - - -def test_first_interrupt_geo() -> None: - with EnvironmentContext() as env: - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - add_stage_variable("stage_model", Spherical) - - mover = MoverGeo( - name="example", - location=GeodeticLocation(*SAN_FRAN), - speed=10, - fuel=500000, - fuel_burn=10, - ) - waypoints = [GeodeticLocation(*x) for x in WAYPOINTS_GEO] - mover.set_knowledge("destinations", waypoints) - task = MoveTask() - proc = task.run(actor=mover) - env.run(until=40) - proc.interrupt(cause="restart") - env.run() - assert pytest.approx(env.now) == (164.81767330428073 * 2 - 40) - d = mover.location - waypoints[-1] - assert pytest.approx(d) == 0 - - -def test_second_interrupt_geo() -> None: - with EnvironmentContext() as env: - add_stage_variable("altitude_units", "ft") - add_stage_variable("distance_units", "nmi") - add_stage_variable("stage_model", Spherical) - - mover = MoverGeo( - name="example", - location=GeodeticLocation(*SAN_FRAN), - speed=10, - fuel=500000, - fuel_burn=10, - ) - waypoints = [GeodeticLocation(*x) for x in WAYPOINTS_GEO] - mover.set_knowledge("destinations", waypoints) - task = MoveTask() - proc = task.run(actor=mover) - env.run(until=130) - proc.interrupt(cause="restart") - env.run() - assert pytest.approx(env.now) == (130 + 69.635346608) - d = mover.location - waypoints[-1] - assert pytest.approx(d) == 0 diff --git a/src/upstage_des/test/test_state_piggyback.py b/src/upstage_des/test/test_state_piggyback.py deleted file mode 100644 index 675e4ad..0000000 --- a/src/upstage_des/test/test_state_piggyback.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -import pytest - -from upstage_des.actor import Actor -from upstage_des.api import Task, UpstageError, Wait -from upstage_des.base import EnvironmentContext -from upstage_des.states import LinearChangingState, State -from upstage_des.type_help import TASK_GEN - - -class Piggy(Actor): - a_level = LinearChangingState(recording=True) - location = State[str]() - - -class Rider(Actor): - b_level = LinearChangingState(recording=True) - location = State[str]() - piggy = State[Piggy]() - - -class RiderTask(Task): - def task(self, *, actor: Rider) -> TASK_GEN: - piggy = actor.piggy - actor.activate_mimic_state( - self_state="b_level", - mimic_state="a_level", - mimic_actor=piggy, - task=self, - ) - yield Wait(1.0) - actor.deactivate_mimic_state( - self_state="b_level", - task=self, - ) - - -class RiderTaskTwo(Task): - def task(self, *, actor: Rider) -> TASK_GEN: - actor.activate_state( - state="b_level", - rate=2, - task=self, - ) - yield Wait(5.0) - actor.deactivate_all_states(task=self) - - -def test_simple() -> None: - with EnvironmentContext() as env: - pig = Piggy(name="Piggy", a_level=1, location="here") - ride = Rider(name="Rider", b_level=2, location="there", piggy=pig) - - assert ride.location == "there" - ride.activate_mimic_state( - self_state="location", - mimic_state="location", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - assert ride.location == "here" - pig.location = "way over there" - assert ride.location == "way over there" - - ride.deactivate_mimic_state(self_state="location", task="None") # type: ignore [arg-type] - assert ride.location == "way over there" - - # a later test will check this differently - assert not hasattr(ride, "_location_history") - - assert ride.b_level == 2 - ride.activate_mimic_state( - self_state="b_level", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - pig.activate_state( - state="a_level", - rate=2, - task=None, # type: ignore [arg-type] - ) - assert ride.b_level == 1 - env.run(until=4) - - pig.deactivate_all_states(task=None) # type: ignore [arg-type] - - assert ride.b_level == 9 - ride.deactivate_all_mimic_states(task="None") # type: ignore [arg-type] - assert ride.b_level == 9 - - ride.activate_state( - state="b_level", - rate=1, - task=None, # type: ignore [arg-type] - ) - env.run(until=5) - assert ride.b_level == 10 - assert pig.a_level == 9 - - -def test_rehearsing() -> None: - with EnvironmentContext(): - pig = Piggy(name="Piggy", a_level=1, location="here") - ride = Rider(name="Rider", b_level=2, location="there", piggy=pig) - - task = RiderTask() - with pytest.raises(UpstageError): - task.rehearse(actor=ride) - - -def test_rehearse_from_piggyback() -> None: - with EnvironmentContext() as env: - pig = Piggy(name="Piggy", a_level=1, location="here") - ride = Rider(name="Rider", b_level=2, location="there", piggy=pig) - - assert ride.b_level == 2 - ride.activate_mimic_state( - self_state="b_level", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - pig.activate_state( - state="a_level", - rate=2, - task=None, # type: ignore [arg-type] - ) - assert ride.b_level == 1 - env.run(until=4) - pig.deactivate_all_states(task=None) # type: ignore [arg-type] - - # Do not deactivate, instead call RiderTaskTwo - # Pig went a_level to 9 - task = RiderTaskTwo() - new_ride = task.rehearse(actor=ride) - assert new_ride.b_level == 19 - assert ride.b_level == 9 - assert pig.a_level == 9 - - ride.deactivate_mimic_state(self_state="b_level", task="None") # type: ignore [arg-type] - pig.a_level = 12 - assert new_ride.b_level == 19 - assert ride.b_level == 9 - - -def test_double_mimic() -> None: - with EnvironmentContext(): - pig = Piggy(name="Piggy", a_level=1, location="here") - ride = Rider(name="Rider", b_level=2, location="there", piggy=pig) - - ride.activate_mimic_state( - self_state="b_level", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - - with pytest.raises(UpstageError): - ride.activate_mimic_state( - self_state="b_level", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - - -def test_interrupt_deactivate() -> None: - with EnvironmentContext() as env: - pig = Piggy(name="Piggy", a_level=1, location="here") - ride = Rider(name="Rider", b_level=2, location="there", piggy=pig) - - ride.activate_mimic_state( - self_state="b_level", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - task = RiderTaskTwo() - task.run(actor=ride) - env.run() - - -def test_record() -> None: - with EnvironmentContext(): - pig = Piggy(name="Piggy", a_level=1, location="here") - - class Rec(Actor): - state = State[int](recording=True) - - class Rec2(Actor): - state = State[int](recording=False) - - ride = Rec( - name="another rider", - state=3, - ) - ride1 = Rec( - name="another rider a", - state=3, - ) - ride2 = Rec2( - name="another rider 2", - state=3, - ) - - ride.activate_mimic_state( - self_state="state", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - - ride1.activate_mimic_state( - self_state="state", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - - ride2.activate_mimic_state( - self_state="state", - mimic_state="a_level", - mimic_actor=pig, - task="None", # type: ignore [arg-type] - ) - - pig.a_level = 23 - assert ride.state == 23 - assert ride1.state == 23 - assert ride2.state == 23 - assert "state" in ride._state_histories - assert "state" in ride1._state_histories - assert "state" not in ride2._state_histories - assert ride._state_histories["state"][1] == (0, 23) - assert ride1._state_histories["state"][1] == (0, 23) diff --git a/src/upstage_des/test/test_stepped_motion.py b/src/upstage_des/test/test_stepped_motion.py deleted file mode 100644 index 31d2f27..0000000 --- a/src/upstage_des/test/test_stepped_motion.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from typing import Protocol - -import pytest - -import upstage_des.api as UP -from upstage_des.motion import SteppedMotionManager -from upstage_des.type_help import TASK_GEN -from upstage_des.utils import waypoint_time_and_dist - - -class LocatedActor(Protocol): - location: UP.CartesianLocation - - -class StaticSensor(UP.Actor): - radius = UP.State[float](valid_types=float, default=2.0) - history = UP.State[list](default_factory=list) - location = UP.State[UP.CartesianLocation](valid_types=(UP.CartesianLocation,)) - - def entity_entered_range(self, detected: LocatedActor) -> None: - dist = self.location - detected.location - self.history.append((self.env.now, "saw", detected.location, dist)) - - def entity_exited_range(self, detected: LocatedActor) -> None: - dist = self.location - detected.location - self.history.append((self.env.now, "lost", detected.location, dist)) - - -class Mover(UP.Actor): - location = UP.CartesianLocationChangingState() - history = UP.State[list](default_factory=list, recording=False) - radius = UP.State[float](default=2.0) - visible = UP.DetectabilityState(default=True) - speed = UP.State[float](default=1.0) - - def entity_entered_range(self, detected: LocatedActor) -> None: - dist = self.location - detected.location - self.history.append((self.env.now, "saw", detected, dist)) - - def entity_exited_range(self, detected: LocatedActor) -> None: - dist = self.location - detected.location - self.history.append((self.env.now, "lost", detected, dist)) - - -class DoMotion(UP.Task): - waypoints: list[UP.CartesianLocation] - - # Hacking together, assuming start location is (2, 1, 0) - def task(self, *, actor: Mover) -> TASK_GEN: - waypoints = [x.copy() for x in self.waypoints] - time, dist = waypoint_time_and_dist(actor.location, waypoints, actor.speed) - actor.activate_state( - state="location", - task=self, - speed=actor.speed, - waypoints=waypoints, - ) - yield UP.Wait(time) - actor.deactivate_all_states(task=self) - - -class EndDetectable(UP.Task): - time: float - - def task(self, *, actor: Mover) -> TASK_GEN: - yield UP.Wait(self.time) - actor.visible = False - - -class StartDetectable(UP.Task): - time: float - - def task(self, *, actor: Mover) -> TASK_GEN: - yield UP.Wait(self.time) - actor.visible = True - - -def test_basic_functions() -> None: - with UP.EnvironmentContext() as env: - motion = SteppedMotionManager(0.01) - UP.add_stage_variable("motion_manager", motion) - - sense = StaticSensor( - name="Static", - radius=1.7, - location=UP.CartesianLocation(0, 0, 0), - ) - move = Mover( - name="A_Mover", - location=UP.CartesianLocation(2, 1, 0), - ) - - motion.add_sensor(sense, "radius", "location") - # This line is handled automatically now - # motion.add_detectable(move, "location") - - waypoints = [ - UP.CartesianLocation(1, 1, 1), - UP.CartesianLocation(0, 0, 2), - ] - - move_task = DoMotion() - move_task.waypoints = waypoints - move_task.run(actor=move) - motion.run() - env.run() - assert len(sense.history[0]) == 4, "Wrong intersection history" - - -def test_dual_sense() -> None: - with UP.EnvironmentContext() as env: - motion = SteppedMotionManager(0.01) - UP.add_stage_variable("motion_manager", motion) - - move1 = Mover( - name="A_Mover1", - location=UP.CartesianLocation(-3, 0, 1), - radius=2, - ) - move2 = Mover( - name="A_Mover2", - location=UP.CartesianLocation(3, 0, 0.5), - radius=1, - ) - - for obj in [move1, move2]: - motion.add_sensor(obj, "radius", "location") - motion.add_detectable(obj, "location") - - waypoints1 = [ - UP.CartesianLocation(3, 0, 0.5), - ] - waypoints2 = [ - UP.CartesianLocation(-3, 0, 1), - ] - - move_task1 = DoMotion() - move_task1.waypoints = waypoints1 - move_task1.run(actor=move1) - - move_task2 = DoMotion() - move_task2.waypoints = waypoints2 - move_task2.run(actor=move2) - - motion.run() - env.run() - assert len(move1.history[0]) == 4, "Wrong intersection history" - assert len(move2.history[0]) == 4, "Wrong intersection history" - assert move1.history[0][2] is move2 - assert move1.history[1][2] is move2 - assert move2.history[0][2] is move1 - assert move2.history[1][2] is move1 - # different distances - assert move1.history[0][3] != move2.history[0][3] - # but correct distances - within a loose tolerance - assert pytest.approx(move1.history[0][3], abs=0.1) == 2 - assert pytest.approx(move2.history[0][3], abs=0.1) == 1 - - -def test_detectability_change() -> None: - with UP.EnvironmentContext() as env: - motion = SteppedMotionManager(0.01) - UP.add_stage_variable("motion_manager", motion) - - move1 = Mover( - name="A_Mover1", - location=UP.CartesianLocation(-3, 0, 1), - radius=2, - ) - move2 = Mover( - name="A_Mover2", - location=UP.CartesianLocation(3, 0, 0.5), - radius=1, - ) - - for obj in [move1, move2]: - motion.add_sensor(obj, "radius", "location") - motion.add_detectable(obj, "location") - - waypoints1 = [ - UP.CartesianLocation(3, 0, 0.5), - ] - waypoints2 = [ - UP.CartesianLocation(-3, 0, 1), - ] - - move_task1 = DoMotion() - move_task1.waypoints = waypoints1 - move_task1.run(actor=move1) - - move_task2 = DoMotion() - move_task2.waypoints = waypoints2 - move_task2.run(actor=move2) - - task_end = EndDetectable() - task_end.time = 2.9 - task_end.run(actor=move1) - - task_start = StartDetectable() - task_start.time = 3.1 - task_start.run(actor=move1) - - motion.run() - env.run() - assert len(move1.history[0]) == 4, "Wrong intersection history" - assert len(move2.history[0]) == 4, "Wrong intersection history" - assert move1.history[0][2] is move2 - assert move1.history[1][2] is move2 - assert move2.history[0][2] is move1 - assert move2.history[1][2] is move1 - assert move2.history[1][0] == 2.9 - assert move2.history[2][0] == 3.1 - assert pytest.approx(move2.history[2][0], abs=0.01) == 3.1 - - -if __name__ == "__main__": - test_basic_functions() diff --git a/src/upstage_des/test/test_stores.py b/src/upstage_des/test/test_stores.py deleted file mode 100644 index f45d62d..0000000 --- a/src/upstage_des/test/test_stores.py +++ /dev/null @@ -1,282 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from collections.abc import Callable -from typing import Any - -import pytest -from simpy import Environment, Store - -from upstage_des.actor import Actor -from upstage_des.base import EnvironmentContext -from upstage_des.events import FilterGet -from upstage_des.resources.monitoring import ( - SelfMonitoringContainer, - SelfMonitoringFilterStore, - SelfMonitoringReserveContainer, - SelfMonitoringSortedFilterStore, - SelfMonitoringStore, -) -from upstage_des.resources.reserve import ReserveContainer -from upstage_des.resources.sorted import SortedFilterGet, SortedFilterStore -from upstage_des.type_help import SIMPY_GEN - -MAX_RUN_TIME = 10.0 - - -def getter( - env: Environment, store: Store, wait: float = 1.0, cback: Callable | None = None, **kwargs: Any -) -> SIMPY_GEN: - yield env.timeout(wait) - get = store.get(**kwargs) - if cback is not None: - get.callbacks.append(cback) - item = yield get - return item - - -def sorted_filter_getter( - env: Environment, - store: SortedFilterStore, - wait: float, - filter: Callable[[Any], bool], - sorter: Callable[[Any], Any] | None = None, - reverse: bool = False, -) -> SIMPY_GEN: - yield env.timeout(wait) - evt = SortedFilterGet( - store, - filter, - sorter, - reverse, - ) - item = yield evt.as_event() - return item - - -def putter( - env: Environment, - store: Store, - item: Any, - wait: float = 0.0, - cback: Callable | None = None, - **kwargs: Any, -) -> SIMPY_GEN: - yield env.timeout(wait) - put = store.put(item, **kwargs) - if cback is not None: - put.callbacks.append(cback) - yield put - - -def test_notifying_store() -> None: - notifications: list[Any] = [] - - def callback(*args: Any, **kwargs: Any) -> None: - assert len(kwargs) == 0 - notifications.append(args) - - with EnvironmentContext() as env: - store = Store(env=env) - - def sim() -> SIMPY_GEN: - item = "an item" - yield env.process(putter(env, store, item, cback=callback)) - retrieved_item = yield env.process(getter(env, store, cback=callback)) - assert item == retrieved_item - - env.process(sim()) - - env.run(until=MAX_RUN_TIME) - - assert len(notifications) == 2 - - -def test_sorted_filter_store() -> None: - with EnvironmentContext() as env: - store = SortedFilterStore(env=env) - - def get_proc() -> SIMPY_GEN: - item = yield env.process( - getter( - env, - store=store, - filter=lambda x: isinstance(x, int | float), - sorter=lambda x: (-x,), - wait=0.0, - ) - ) - return item - - def sim() -> SIMPY_GEN: - env.process(putter(env, store, 10, wait=0.0)) - env.process(putter(env, store, 1, wait=0.0)) - - item = yield env.process(get_proc()) - - assert item == 10 - - env.process(sim()) - - env.run(until=MAX_RUN_TIME) - - -def test_sorted_filter_store_upstage_get() -> None: - with EnvironmentContext() as env: - store = SortedFilterStore(env=env) - - def get_proc() -> SIMPY_GEN: - item = yield env.process( - sorted_filter_getter( - env, - store=store, - filter=lambda x: isinstance(x, int | float), - sorter=lambda x: x, - reverse=True, - wait=0.0, - ) - ) - return item - - def sim() -> SIMPY_GEN: - env.process(putter(env, store, 10, wait=0.0)) - env.process(putter(env, store, 1, wait=0.0)) - - item = yield env.process(get_proc()) - - assert item == 10 - - env.process(sim()) - - env.run(until=MAX_RUN_TIME) - - -def test_reserve_store() -> None: - with EnvironmentContext() as env: - store = ReserveContainer( - env=env, - capacity=10, - init=10, - ) - assert store.available == 10 - - class R(Actor): ... - - requestor = R(name="wanter") - - req = store.reserve(requestor, 8, expiration=12) - assert req is not False - req1 = store.reserve(R(name="other"), 8) - assert not req1 - - assert store.available == 2 - assert len(store._queued) == 1 - env.run(until=1) - store.cancel_request(requestor) - assert len(store._queued) == 0 - assert store.available == 10 - - req = store.reserve(requestor, 8, expiration=8) - assert req is not False - env.run(until=13) - assert len(store.queued) == 0 - assert store.available == 10 - with pytest.raises(ValueError): - store.take(requestor) - - req = store.reserve(requestor, 8, expiration=8) - env.run(until=14) - assert store.take(requestor) == 8 - env.run(until=30) - assert store.available == 2 - - with pytest.raises(ValueError): - store.put(100, capacity_increase=False) - - store.put(3) - assert store.available == 5 - - -def test_self_monitoring_filter_store() -> None: - with EnvironmentContext() as env: - store = SelfMonitoringFilterStore(env=env) - - def filter(item: str) -> bool: - return "Another" in item - - def proc() -> SIMPY_GEN: - yield store.put("The Item") - yield store.put("Another Item") - evt = FilterGet( - store, - filter, - ) - item = yield evt.as_event() - return item - - def sim() -> SIMPY_GEN: - item = yield env.process(proc()) - assert item == "Another Item" - - env.process(sim()) - - env.run(until=MAX_RUN_TIME) - - -def test_self_monitoring_sorted_filter_store() -> None: - with EnvironmentContext() as env: - store = SelfMonitoringSortedFilterStore(env=env) - - def get_proc() -> SIMPY_GEN: - item = yield env.process( - getter( - env, - store=store, - filter=lambda x: isinstance(x, int | float), - sorter=lambda x: (-x,), - wait=0.0, - ), - ) - return item - - def sim() -> SIMPY_GEN: - env.process(putter(env, store, 10, wait=0.0)) - env.process(putter(env, store, 1, wait=0.0)) - - item = yield env.process(get_proc()) - - assert item == 10 - - env.process(sim()) - - env.run(until=MAX_RUN_TIME) - - -def test_self_monitoring_store() -> None: - with EnvironmentContext() as env: - SelfMonitoringStore(env) - env.run(until=MAX_RUN_TIME) - - -def test_self_monitoring_reserve_store() -> None: - with EnvironmentContext() as env: - SelfMonitoringReserveContainer(env) - env.run(until=MAX_RUN_TIME) - - -def test_self_monitoring_container() -> None: - with EnvironmentContext() as env: - con = SelfMonitoringContainer(env, capacity=10) - - def sim() -> SIMPY_GEN: - evt = con.put(5) - yield evt - evt = con.get(3) - yield evt - - env.process(sim()) - env.run() - assert len(con._quantities) == 3 - assert [0, 5, 2] == [x[1] for x in con._quantities] diff --git a/src/upstage_des/test/test_task.py b/src/upstage_des/test/test_task.py deleted file mode 100644 index 2d9b1b3..0000000 --- a/src/upstage_des/test/test_task.py +++ /dev/null @@ -1,383 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from inspect import isgeneratorfunction -from typing import Any, cast - -import pytest -from simpy import Environment, Process - -from upstage_des.actor import Actor -from upstage_des.api import InterruptStates, SimulationError -from upstage_des.base import EnvironmentContext -from upstage_des.events import Wait -from upstage_des.states import LinearChangingState, State -from upstage_des.task import Task, TerminalTask -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class ActorForTest(Actor): - dummy = State[float](recording=True) - - -class ActorChangeForTest(Actor): - dummy = LinearChangingState() - - -class WorkingTask(Task): - times: list[float] - - def task(self, *, actor: ActorForTest) -> TASK_GEN: - for wait_period in self.times: - the_event = Wait(wait_period) - yield the_event - actor.dummy += wait_period - - -class ChangingTask(Task): - times: list[float] - rate: float - - def task(self, *, actor: ActorForTest) -> TASK_GEN: - for t in self.times: - the_event = Wait(t) - actor.activate_state(state="dummy", task=self, rate=self.rate) - yield the_event - actor.deactivate_state(state="dummy", task=self) - - -class Actor2Test(Actor): - dummy = State[Any]() - - -class WorkingTask2(Task): - times: list[float] - log: list[str] - - def task(self, *, actor: ActorChangeForTest) -> TASK_GEN: - for wait_period in self.times: - wait_event = Wait(wait_period) - self.log.append( - f"{self.env.now}: {self.__class__.__name__} " - f"waiting {wait_period}, value={actor.dummy}" - ) - yield wait_event - self.log.append( - f"{self.env.now}: {self.__class__.__name__} " - f"finished waiting {wait_period}, " - f"value={actor.dummy}" - ) - actor.dummy += wait_period - - -class ChangingTask2(Task): - times: list[float] - rate: float - log: list[str] - - def task(self, *, actor: ActorForTest | ActorChangeForTest) -> TASK_GEN: - for wait_period in self.times: - wait_event = Wait(wait_period) - actor.activate_state(state="dummy", task=self, rate=self.rate) - actor.set_knowledge("example for logging", "a value", overwrite=True) - self.log.append( - f"{self.env.now}: {self.__class__.__name__} " - f"waiting {wait_period}, value={actor.dummy}" - ) - yield wait_event - self.log.append( - f"{self.env.now}: {self.__class__.__name__} finished " - f"waiting {wait_period}, value={actor.dummy}" - ) - actor.deactivate_state(state="dummy", task=self) - - -def _task_runner(env: Environment, rate: float, timeout_point: float) -> SIMPY_GEN: - use_actor = ActorChangeForTest(name="testing", dummy=0.0, debug_log=True) - times = [1.0, 2.0] - - task_object = ChangingTask2() - task_object.times = times - task_object.rate = rate - task_object.log = [] - - task_generator = task_object.run( - actor=use_actor, - ) - timeout = env.timeout(timeout_point) - - yield task_generator | timeout - - if task_generator.is_alive: - task_generator.interrupt("cancelling") - - return use_actor - - -def test_creation() -> None: - with EnvironmentContext(): - _ = WorkingTask() - - -def test_failures_for_tasks_with_simpy_events() -> None: - with EnvironmentContext() as env: - actor = ActorForTest(name="testing", dummy=0) - - class BrokenTask(Task): - def task(self, *, actor: ActorForTest) -> TASK_GEN: - yield self.env.timeout(1.0) # type: ignore [misc, union-attr] - - # msg = "*Task is yielding objects without `as_event`*" - with pytest.raises(SimulationError): # , match=msg): - the_task = BrokenTask() - _ = the_task.run( - actor=actor, - ) - env.run() - - # msg = "*'MockEnvironment' object has no attribute 'timeout'*" - with pytest.raises(AttributeError): # , match=msg): - the_task = BrokenTask() - the_task.rehearse( - actor=actor, - ) - - -def test_failures_for_tasks_with_incorrect_events() -> None: - with EnvironmentContext(): - actor = ActorForTest(name="testing", dummy=0) - - class BlankEvent: - def __init__(self, **kwargs: Any) -> None: - pass - - class BrokenTask(Task): - event_class: type - - def task(self, *, actor: ActorForTest) -> TASK_GEN: - yield self.event_class() - - # msg = '*must be a subclass of BaseEvent*' - with pytest.raises(SimulationError): - task_instance = BrokenTask() - task_instance.event_class = BlankEvent - task_instance.rehearse( - actor=actor, - ) - - -def test_running_as_rehearsal() -> None: - with EnvironmentContext() as env: - actor = ActorForTest(name="testing", dummy=0) - times = [1.0, 2.0] - task_object = WorkingTask() - task_object.times = times - rehearse_function = task_object.rehearse - - # assert that the task is not a generator - msg = "Task when tested should not be a generator" - assert not isgeneratorfunction(rehearse_function), msg - - result = rehearse_function( - actor=actor, - ) - msg = "Result of testing task must be an actor" - assert isinstance(result, Actor), msg - - assert env.now == 0, "Environment time must not increase" - - msg = "Actor returned by task test is missing expected state changes" - assert result.dummy == 3, msg - - msg = "Actor returned by task test needs to keep recorded data" - assert len(result._state_histories["dummy"]) == 3, msg - assert result._state_histories["dummy"][0] == (0, 0), msg - assert result._state_histories["dummy"][2] == (sum(times), sum(times)), msg - - msg = "No linkage between original actor and dummy actor" - assert actor.dummy == 0, msg - - msg = "Environment not properly reset" - assert task_object.env is env, msg - - -def test_running() -> None: - with EnvironmentContext() as env: - actor = ActorForTest(name="testing", dummy=0) - times = [1.0, 2.0] - - task_object = WorkingTask() - task_object.times = times - task_process = task_object.run( - actor=actor, - ) - env.run() - assert env.now == 3, "Environment time must increase" - assert actor.dummy == 3, "Actor state must change" - assert isinstance(task_process, Process), "Task process is not an instance of simpy.Process" - - -def test_interrupting() -> None: - with EnvironmentContext() as env: - timeout_point = 0.5 - rate = 0.5 - proc = env.process( - _task_runner( - env, - rate=rate, - timeout_point=timeout_point, - ) - ) - env.run() - actor = cast(ActorForTest, proc.value) - msg = "Task interruption ended at the wrong time" - assert actor.dummy == timeout_point * rate, msg - - -def test_interrupting_two() -> None: - # Do the timeout right when a time will end - with EnvironmentContext() as env: - timeout_point = 1.0 - rate = 3.5 - proc = env.process( - _task_runner( - env=env, - rate=rate, - timeout_point=timeout_point, - ) - ) - env.run() - actor = cast(ActorForTest, proc.value) - msg = "Task interruption ended at the wrong time" - assert actor.dummy == timeout_point * rate, msg - - -def test_simultaneous_task() -> None: - with EnvironmentContext() as env: - actor = ActorChangeForTest(name="testing", dummy=0.0) - - def task_runner( - *, - task_class: type[WorkingTask2 | ChangingTask2], - interrupt_time: float, - **task_kwargs: Any, - ) -> SIMPY_GEN: - task = task_class() - task.log = [] - for k, v in task_kwargs.items(): - setattr(task, k, v) - running_task = task.run( - actor=actor, - ) - - timeout = env.timeout(interrupt_time) - - yield running_task | timeout - - if running_task.is_alive: - running_task.interrupt("cancelling") - - return actor - - _ = env.process( - task_runner( - task_class=ChangingTask2, - rate=1.0, - times=[1.0, 3.0, 5.0, 6.0], - interrupt_time=10.0, - ) - ) - - _ = env.process( - task_runner( - task_class=WorkingTask2, - times=[2.0, 2.0, 2.0, 10.0], - interrupt_time=12.0, - ) - ) - - env.run(until=20.0) - - -def test_terminal_task_run( - task_objects: tuple[type[TerminalTask], type[TerminalTask], type[Actor]], -) -> None: - EndPoint, EndPointBase, Dummy = task_objects - - with EnvironmentContext() as env: - actor = Dummy(name="x", status="Good", debug_log=True) - task = EndPoint() - - proc = task.run(actor=actor) - env.run() - assert env.now == 0 - - assert "The Message" in actor._debug_log[-1][1] - - with pytest.raises(SimulationError, match=".+Cannot interrupt a terminal.+"): - proc.interrupt() - env.run() - - actor = Dummy(name="x", status="Good", debug_log=True) - task = EndPointBase() - proc = task.run(actor=actor) - env.run() - assert env.now == 0 - - assert "Entering terminal task:" in actor._debug_log[-1][1] - - -def test_terminal_task_rehearse( - task_objects: tuple[type[TerminalTask], type[TerminalTask], type[Actor]], -) -> None: - EndPoint, _, Dummy = task_objects - with EnvironmentContext(): - actor = Dummy(name="x", status="Good") - task = EndPoint() - - clone = task.rehearse(actor=actor) - assert clone.env.now == task._time_to_complete - - -class Dummy(Actor): - status = State[str]() - rate = State[float]() - changer = LinearChangingState(recording=True) - - -class Restartable(Task): - def task(self, *, actor: Dummy) -> TASK_GEN: - actor.activate_state( - state="changer", - task=self, - rate=actor.rate, - ) - self.set_marker("change to test") - yield Wait(10.0) - actor.deactivate_all_states(task=self) - - def on_interrupt(self, *, actor: Dummy, cause: Any) -> InterruptStates: - if cause == "restart": - return self.INTERRUPT.RESTART - else: - return self.INTERRUPT.END - - -def test_restart() -> None: - with EnvironmentContext() as env: - actor = Dummy( - name="Example", - status="available", - rate=2.3, - changer=0.0, - debug_log=True, - ) - - task = Restartable() - proc = task.run(actor=actor) - env.run(until=3.4) - proc.interrupt(cause="restart") - env.run() - assert pytest.approx(actor.changer) == 2.3 * (3.4 + 10) diff --git a/src/upstage_des/test/test_task_network.py b/src/upstage_des/test/test_task_network.py deleted file mode 100644 index b6292b1..0000000 --- a/src/upstage_des/test/test_task_network.py +++ /dev/null @@ -1,914 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from collections.abc import Sequence - -import pytest -from simpy import Environment -from simpy import Resource as sp_resource -from simpy import Store as sp_store - -from upstage_des.api import ( - Actor, - Any, - CartesianLocationChangingState, - DecisionTask, - EnvironmentContext, - Event, - Get, - InterruptStates, - LinearChangingState, - Put, - ResourceHold, - State, - Task, - TaskLinks, - TaskNetworkFactory, - Wait, - add_stage_variable, -) -from upstage_des.data_types import CartesianLocation, Location -from upstage_des.task import process -from upstage_des.type_help import SIMPY_GEN, TASK_GEN - - -class Base: - def __init__( - self, - env: Environment, - name: str, - x: float, - y: float, - num_runways: int = 1, - parking_max: int = 10, - ) -> None: - self.env = env - self.name = name - self.location = CartesianLocation(x=x, y=y) - self.runway = sp_resource(self.env, capacity=num_runways) - self.maintenance_queue = sp_store(self.env) - self.parking = sp_store(self.env, parking_max) - self.parking_tokens = sp_store(self.env, parking_max) - self.parking_tokens.items = [(self, self.parking_tokens, i) for i in range(parking_max)] - self.operational = True - - # yes, this is weird - self.location = CartesianLocation(x, y) - - self.parking_max = parking_max - self.curr_parked: int = 0 - self.claimed_parking: int = 0 - self._parking_claims: list[Any] = [] - self._parked: list[Any] = [] - - def claim_parking(self, plane: Any) -> None: - self._parking_claims.append(plane) - self.claimed_parking += 1 - - def has_parking(self, plane: Any) -> int: - return self.curr_parked + self.claimed_parking < self.parking_max - - @process - def run_maintenance(self) -> SIMPY_GEN: - while True: - plane = yield Get(self.maintenance_queue).as_event() - yield Wait(1.6).as_event() - mx_wait = plane.get_knowledge("mx_wait") - # alter the plane's code - plane.code = 0 - mx_wait.succeed() - - def __repr__(self) -> str: - return f"{self.name}:{super().__repr__()}" - - -class World: - """A helper class for data storage and environment analysis""" - - def __init__(self, bases: Sequence[Base]) -> None: - self.bases = bases - - def nearest_base(self, loc: CartesianLocation) -> Base: - b = min(self.bases, key=lambda x: x.location - loc) - return b - - def bases_by_location(self, loc: CartesianLocation) -> Base: - x, y = (loc.x, loc.y) - return [b for b in self.bases if b.location.x == x and b.location.y == y][0] - - -class Aircraft(Actor): - base = State[Base | None](default=None) - location = CartesianLocationChangingState(recording=True) - speed = State[float]() - landing_time = State[float]() - takeoff_time = State[float]() - code = State[int]() - fuel = LinearChangingState(recording=True) - fuel_burn = State[float]() - parking_token = State[int]() - parking_spot = State[int]() - command_data = State[Any]() - world = State[World]() - - def calculate_bingo(self, max_time: float = float("inf")) -> float: - # get the farthest base - farthest: Base = max(self.world.bases, key=lambda x: self.location - x.location) - dist = self.location - farthest.location - time = dist / self.speed - fuel_needed = self.fuel_burn * time - time_until_bingo = (self.fuel - fuel_needed) / self.fuel_burn - return min(time_until_bingo, max_time) - - -class CodeFour(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - wait = Event(rehearsal_time_to_complete=float("inf")) - yield wait - - -class GroundWait(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - wait = Event(rehearsal_time_to_complete=0.0) - yield wait - - -class GroundTakeoffWait(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - destination = self.get_actor_knowledge(actor, "destination") - if not isinstance(destination, Location): - destination = destination.location - arrival_time = self.get_actor_knowledge(actor, "arrival time") - if arrival_time is None: - wait_time = 0 - else: - distance = destination - actor.location - flight_time = distance / actor.speed - wait_time = arrival_time - flight_time - yield Wait(wait_time) - - -class Takeoff(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - base = actor.base - assert base is not None - runway_request = ResourceHold(base.runway) - yield runway_request - takeoff_time = Wait(actor.takeoff_time) - yield takeoff_time - yield runway_request - # HOW WOULD THIS WORK IN TRIAL MODE? - # what if it's a parking spot token? A token state? - parking_event = Put(base.parking_tokens, actor.parking_spot) - yield parking_event - actor.parking_spot = -1 - # set our current location as above the base - actor.base = None - - def on_interrupt(self, *, actor: Aircraft, cause: str) -> InterruptStates: - director = self.get_actor_knowledge(actor, "director") - if director is not None: - director.report_failure(actor) - return self.INTERRUPT.END - - -class Fly(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - destination = self.get_actor_knowledge(actor, "destination") - if not isinstance(destination, Location): - destination = destination.location - actor.activate_state( - state="location", - task=self, - speed=actor.speed, - waypoints=[destination], - ) - actor.activate_state( - state="fuel", - task=self, - rate=-actor.fuel_burn, - ) - - # assume that locations can do this - distance = destination - actor.location - time = distance / actor.speed - fly_wait = Wait(time) - - yield fly_wait - - actor.deactivate_all_states(task=self) - intent = self.get_actor_knowledge(actor, "intent") - next_task = self.get_actor_next_task(actor) - - # if our intent is to land, make sure next task is a landing check - if intent == "land" and next_task != "LandingCheck": - self.clear_actor_task_queue(actor) - self.set_actor_task_queue( - actor, - [ - "LandingCheck", - ], - ) - - def on_interrupt(self, *, actor: Aircraft, cause: str) -> InterruptStates: - # For testing a restart, this flying is fine, since there is only one - # final destination. - if cause == "restart": - actor.speed = 0.5 - return self.INTERRUPT.RESTART - else: - return self.INTERRUPT.END - - -class Loiter(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - # calculate bingo - time = actor.calculate_bingo() - loiter_wait = Wait(time) - actor.activate_state( - state="fuel", - task=self, - fuel_burn_rate=-actor.fuel_burn, - ) - yield loiter_wait - actor.deactivate_state( - state="fuel", - task=self, - ) - - -class Mission(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - # tell the mission folks you are here - # they'll give you an event to watch to leave - commander = self.get_actor_knowledge(actor, "commander", must_exist=True) - leave_event = commander.arrival(actor) - # set up an alternate bingo leave event - time = actor.calculate_bingo() - bingo_wait = Wait(time) - - stop_mission = Any(leave_event, bingo_wait) - actor.activate_state( - state="fuel", - task=self, - fuel_burn_rate=-actor.fuel_burn, - ) - yield stop_mission - actor.deactivate_all_states(task=self) - - -class LandingLocationSelection(DecisionTask): - def rehearse_decision(self, *, actor: Aircraft) -> None: - base = actor.stage.world.bases[0] - self.set_actor_knowledge(actor, "destination", base) - self.set_actor_knowledge(actor, "intent", "land") - - def make_decision(self, *, actor: Aircraft) -> None: - # These kinds of cognitive tasks must be zero-time (no yields!) - landable_bases = [b for b in actor.stage.world.bases if b.has_parking(actor)] - base = landable_bases[0] - - self.set_actor_knowledge(actor, "destination", base) - self.set_actor_knowledge(actor, "intent", "land") - - -class LandingLocationPrep(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - base = self.get_actor_knowledge(actor, "destination") - token_event = Get(base.parking_tokens) - parking_token = yield token_event - self.set_actor_knowledge(actor, "parking_token", parking_token) - - -class LandingCheck(DecisionTask): - """Task for checking the ability to land at the base an actor is above. - - If landing is available, continue in the task network. Otherwise, reselect a base. - """ - - return_task_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - def rehearse_decision(self, *, actor: Aircraft) -> None: - return None - - def make_decision(self, *, actor: Aircraft) -> None: - # get base from the actor's destination - base = self.get_actor_knowledge(actor, "destination") - # assert that the base can be landed at - if not base.operational: - # clear the queue - self.clear_actor_task_queue(actor) - self.clear_actor_knowledge(actor, "destination") - self.clear_actor_knowledge(actor, "intent") - # set up for a task network path that gets a new place to land - self.set_actor_task_queue(actor, self.return_task_list) - else: - # check that we are landing - msg = f"Actor {actor} not landing after check" - assert self.get_actor_next_task(actor) == "Land", msg - - -class Land(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - base = self.get_actor_knowledge(actor, "destination") - self.clear_actor_knowledge(actor, "destination") - runway_request = ResourceHold(base.runway) - - self.set_marker("pre-runway") - yield runway_request - landing_time = Wait(actor.landing_time) - - self.set_marker("during landing") - yield landing_time - - self.set_marker("post-landing", self.INTERRUPT.IGNORE) - yield runway_request - self.clear_marker() - - self.set_actor_knowledge(actor, "base", base) - # just to help with a 'clear actor from all stores' need? - put_event = Put(base.parking, actor) - self.set_marker("get parking", self.INTERRUPT.IGNORE) - yield put_event - - self.set_marker("post-parking", interrupt_action=self.INTERRUPT.IGNORE) - parking_token = self.get_actor_knowledge(actor, "parking_token") - put_event = Put(base.parking_tokens, parking_token) - yield put_event - - self.clear_actor_knowledge(actor, "parking_token") - self.clear_actor_knowledge(actor, "intent") - - def on_interrupt(self, *, actor: Aircraft, cause: str) -> InterruptStates: - # if interrupted, find a new place to land - # figure out where we were in the task based on the marker - marker = self.get_marker() - self.get_marker_time() - if marker in [ - "pre-runway", - ]: - # We are done with this base - # put the parking token back - parking_token = self.get_actor_knowledge(actor, "parking_token") - if parking_token: - store = parking_token[1] - Put(store, parking_token) - self.clear_actor_knowledge(actor, "parking_token") - # set the task network to try landing again - return_task_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - self.clear_actor_task_queue(actor) - self.set_actor_task_queue(actor, return_task_list) - return self.INTERRUPT.END - elif marker in [ - "during landing", - ]: - # continue on if the cause is benign - if cause == "Code 4": - # clear the knowledge and task queue - parking_token = self.get_actor_knowledge(actor, "parking_token") - if parking_token: - store = parking_token[1] - Put(store, parking_token) - self.clear_actor_knowledge(actor, "parking_token") - self.clear_actor_task_queue(actor) - self.set_actor_task_queue(actor, ["Code4"]) - return self.INTERRUPT.END - else: - return self.INTERRUPT.IGNORE - else: - # other markers mean that the landing was safe so no - # need to do anything - raise ValueError("Shouldn't be here") - - -class MaintenanceWait(Task): - def task(self, *, actor: Aircraft) -> TASK_GEN: - base = self.get_actor_knowledge(actor, "base") - maintenance_put = Put(base.maintenance_queue, actor) - yield maintenance_put - mx_wait = Event(rehearsal_time_to_complete=2.0) - self.set_actor_knowledge(actor, "mx_wait", mx_wait) - yield mx_wait - self.clear_actor_knowledge(actor, "mx_wait") - - def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: - return InterruptStates.IGNORE - - -BASE_LOCATIONS = [ - (10.023, 4.63), - (2.409, 7.279), - (0.529, 11.004), - (6.468, 17.153), - (5.802, 17.215), -] - -task_classes = { - "GroundWait": GroundWait, - "GroundTakeoffWait": GroundTakeoffWait, - "Takeoff": Takeoff, - "Fly": Fly, - "Loiter": Loiter, - "Mission": Mission, - "LandingLocationSelection": LandingLocationSelection, - "LandingLocationPrep": LandingLocationPrep, - "LandingCheck": LandingCheck, - "Land": Land, - "MaintenanceWait": MaintenanceWait, - "Code4": CodeFour, -} -_task_links = { - "GroundWait": [ - "Takeoff", - "Code4", - ], - "Takeoff": [ - "Fly", - "Loiter", - "LandingLocationSelection", - "Code4", - ], - "Loiter": [ - "Fly", - "Mission", - "LandingLocationSelection", - ], - "Fly": [ - "Loiter", - "Mission", - "LandingLocationSelection", - "LandingCheck", - "Fly", - ], - "Mission": [ - "Fly", - "Loiter", - "LandingLocationSelection", - ], - "LandingLocationSelection": [ - "Fly", - "Loiter", - "LandingLocationPrep", - ], - "LandingLocationPrep": [ - "Fly", - "Loiter", - "LandingCheck", - "LandingLocationSelection", - ], - "LandingCheck": [ - "Land", - "Loiter", - "Fly", - "LandingLocationSelection", - ], - "Land": [ - "MaintenanceWait", - "Loiter", - "Code4", - ], - "MaintenanceWait": [ - "GroundWait", - "Code4", - ], - "Code4": [], -} -# quick fix for new task network link style -task_links: dict[str, TaskLinks] = {} -for k, v in _task_links.items(): - new = TaskLinks(default=v[0] if v else None, allowed=v) - task_links[k] = new - - -def _build_test(env: Environment) -> Aircraft: - bases = [] - for i in range(len(BASE_LOCATIONS)): - x, y = BASE_LOCATIONS[i] - b = Base(env, f"Base {i}", x, y) - bases.append(b) - b.run_maintenance() - - world = World(bases) - add_stage_variable("world", world) - - p = Aircraft( - name="my plane", - base=None, - location=CartesianLocation(0, 0), - speed=12, - landing_time=5 / 60, - takeoff_time=10 / 60, - code=2, - fuel=100, - fuel_burn=15, - parking_token=None, - parking_spot=None, - command_data=None, - world=world, - debug_log=True, - ) - - return p - - -def test_plane_bingo() -> None: - with EnvironmentContext() as env: - p = _build_test(env) - bingo_hours = 5.14 - bingo_result = p.calculate_bingo() - assert pytest.approx(bingo_result, abs=0.01) == bingo_hours - - -def test_creating_network() -> None: - with EnvironmentContext(): - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - _ = task_fact.make_network() - - -def test_rehearsing_network() -> None: - with EnvironmentContext() as env: - actor = _build_test(env) - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - net = task_fact.make_network() - - # build arguments for the task list - task_name_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - actor.add_task_network(net) - # start the task network - actor.start_network_loop("plane_net", init_task_name="GroundWait") - env.run() - assert env.now == 0 - - new_actor = net.rehearse_network(actor=actor, task_name_list=task_name_list) - - # Did the new actor do what we wanted it to? - base = new_actor.get_knowledge("base") - base2 = actor.stage.world.bases[0] - # Notice that due to copying the actor, the bases aren't exactly the same - assert base.name == base2.name, "Wrong base selected" - assert len(new_actor._knowledge) == 1, "Too much knowledge left" - assert pytest.approx(new_actor.fuel, abs=0.01) == 86.199 - - # Is the original actor untouched? - assert len(actor._knowledge) == 0, "Actor should not have done anything" - - assert new_actor.code == 2, "Wrong MX code" - - assert new_actor.env.now > 2.0, "Cloned actor environment at the wrong time" - - task = actor.get_running_task("plane_net") - assert task is not None and task.name == "GroundWait" - - -def test_rehearsing_from_actor() -> None: - with EnvironmentContext() as env: - actor = _build_test(env) - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - net = task_fact.make_network() - - # build arguments for the task list - task_name_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - actor.add_task_network(net) - - new_actor = actor.rehearse_network( - "plane_net", - task_name_list, - knowledge={"dummy_know": 8675309}, - ) - - # Make sure the knowledge was set on the copy, but not the original - assert actor.get_knowledge("dummy_know") is None - assert new_actor.get_knowledge("dummy_know") == 8675309 - - # Did the new actor do what we wanted it to? - base = new_actor.get_knowledge("base") - base2 = actor.stage.world.bases[0] - # Notice that due to copying the actor, the bases aren't exactly the same - assert base.name == base2.name, "Wrong base selected" - assert len(new_actor._knowledge) == 2, "Too much knowledge left" - assert pytest.approx(new_actor.fuel, abs=0.01) == 86.199 - - # Is the original actor untouched? - assert len(actor._knowledge) == 0, "Actor should not have done anything" - assert new_actor.code == 2, "Wrong MX code" - - assert new_actor.env.now > 2.0, "Cloned actor environment at the wrong time" - - -def test_running_simple_network() -> None: - with EnvironmentContext() as env: - actor = _build_test(env) - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - net = task_fact.make_network() - - assert str(net) == "Task network: plane_net" - - # build arguments for the task list - task_name_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - # tell the actor the queue its getting - actor.add_task_network(net) - actor.set_task_queue("plane_net", task_name_list) - - # run the queue with the network - net.loop(actor=actor) - env.run() - - base = actor.get_knowledge("base") - base2 = actor.stage.world.bases[0] - assert base is base2, "Wrong base selected" - assert len(actor._knowledge) == 1, "Too much knowledge left" - assert pytest.approx(actor.fuel, abs=0.01) == 86.199 - assert actor.code == 0, "Wrong MX code" - - -def test_interrupting_network() -> None: - with EnvironmentContext() as env: - actor = _build_test(env) - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - net = task_fact.make_network() - - # build arguments for the task list - task_name_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - # tell the actor the queue its getting - actor.add_task_network(net) - actor.set_task_queue("plane_net", task_name_list) - - # create a process that interrupts the plane during different times - def interrupting_proc( - env: Environment, actor: Aircraft, interrupt_time: float - ) -> SIMPY_GEN: - yield env.timeout(interrupt_time) - # get the process - network = actor._task_networks["plane_net"] - assert network._current_task_proc is not None - network._current_task_proc.interrupt(cause="a reason") - - # run the queue with the network - net.loop(actor=actor) - env.process(interrupting_proc(env, actor, 1.0)) - env.run() - - # the plane should land still - assert actor._task_queue["plane_net"] == [], "Actor had tasks left" - assert actor.code == 0, "Actor didn't get maintained" - - -def test_interrupting_network_with_cause() -> None: - with EnvironmentContext() as env: - actor = _build_test(env) - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - net = task_fact.make_network() - - # build arguments for the task list - task_name_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - # tell the actor the queue its getting - actor.add_task_network(net) - actor.set_task_queue("plane_net", task_name_list) - - # create a process that interrupts the plane during different times - def interrupting_proc( - env: Environment, actor: Aircraft, interrupt_time: float - ) -> SIMPY_GEN: - yield env.timeout(interrupt_time) - # get the process - # network = actor._task_networks["plane_net"] - # network._current_task_proc.interrupt(cause="Code 4") - actor.interrupt_network("plane_net", cause="Code 4") - - # run the queue with the network - net.loop(actor=actor) - env.process(interrupting_proc(env, actor, 1.0)) - env.run() - - -def test_interrupting_network_with_restart() -> None: - with EnvironmentContext() as env: - actor = _build_test(env) - task_fact = TaskNetworkFactory( - "plane_net", - task_classes, - task_links, - ) - net = task_fact.make_network() - - # build arguments for the task list - task_name_list = [ - "LandingLocationSelection", - "LandingLocationPrep", - "Fly", - "LandingCheck", - "Land", - "MaintenanceWait", - ] - - # tell the actor the queue its getting - actor.add_task_network(net) - actor.set_task_queue("plane_net", task_name_list) - - # create a process that interrupts the plane during different times - def interrupting_proc( - env: Environment, actor: Aircraft, interrupt_time: float - ) -> SIMPY_GEN: - yield env.timeout(interrupt_time) - # get the process - network = actor._task_networks["plane_net"] - assert network._current_task_name is not None and network._current_task_name == "Fly" - assert network._current_task_proc is not None - network._current_task_proc.interrupt(cause="restart") - - # run the queue with the network - net.loop(actor=actor) - env.process(interrupting_proc(env, actor, 0.1)) - env.run() - # the plane should land still - assert actor._task_queue["plane_net"] == [], "Actor had tasks left" - assert actor.code == 0, "Actor didn't get maintained" - # it should take longer than the cancelled version - assert pytest.approx(env.now, abs=0.0001) == 21.464767 - - -def test_rehearsal_time() -> None: - class Thing(Actor): - the_time = LinearChangingState(recording=True) - - class ThingWait(Task): - def task(self, *, actor: Thing) -> TASK_GEN: - actor.activate_state( - state="the_time", - task=self, - rate=1.0, - ) - yield Wait.from_random_uniform(1.0, 2.0) - actor.deactivate_all_states(task=self) - - with EnvironmentContext(): - tasks = {"ThingWait": ThingWait} - task_links = {"ThingWait": TaskLinks(default="ThingWait", allowed=["ThingWait"])} - factory = TaskNetworkFactory("fact", tasks, task_links) - - thing = Thing(name="Actor", the_time=0) - thing.add_task_network(factory.make_network()) - new_thing = thing.rehearse_network("fact", ["ThingWait", "ThingWait"]) - assert new_thing.env.now == new_thing.the_time, "Bad rehearsal env time" - - -def test_decision_task_hold() -> None: - # Test the conditions found in https://github.com/gtri/upstage/issues/35 - # Looks at zero time holds vs pass-through decision tasks - - # Test for new behavior first. - data = [] - - class Waiter(Task): - def task(self, *, actor: Actor) -> TASK_GEN: - data.append(f"{self.env.now:.1f} >> {actor.name} in Waiter") - yield Wait(1.0) - - class Runner(Task): - def task(self, *, actor: Actor) -> TASK_GEN: - data.append(f"{self.env.now:.1f} >> {actor.name} in Runner") - yield Wait(2.0) - - class Thinker(DecisionTask): - DO_NOT_HOLD = True - - def make_decision(self, *, actor: Actor) -> None: - data.append(f"{self.env.now:.1f} >> {actor.name} in Thinker") - if "one" in actor.name: - self.set_actor_task_queue(actor, ["Waiter"]) - else: - self.set_actor_task_queue(actor, ["Runner"]) - - net = TaskNetworkFactory( - name="Example Net", - task_classes={"Waiter": Waiter, "Runner": Runner, "Thinker": Thinker}, - task_links={ - "Waiter": TaskLinks(default="Thinker", allowed=["Thinker"]), - "Thinker": TaskLinks(default="", allowed=["Waiter", "Runner"]), - "Runner": TaskLinks(default="Thinker", allowed=["Thinker"]), - }, - ) - with EnvironmentContext() as env: - a = Actor(name="Actor one", debug_log=True) - b = Actor(name="Actor two", debug_log=True) - - for actor in [a, b]: - n = net.make_network() - actor.add_task_network(n) - actor.start_network_loop(n.name, "Waiter") - - env.run(until=2) - - expected = [ - "0.0 >> Actor one in Waiter", - "0.0 >> Actor two in Waiter", - "1.0 >> Actor one in Thinker", - "1.0 >> Actor one in Waiter", - "1.0 >> Actor two in Thinker", - "1.0 >> Actor two in Runner", - ] - assert data == expected - - # Reset data in place, test for default behavior - data[:] = [] - - Thinker.DO_NOT_HOLD = False - with EnvironmentContext() as env: - a = Actor(name="Actor one", debug_log=True) - b = Actor(name="Actor two", debug_log=True) - - for actor in [a, b]: - n = net.make_network() - actor.add_task_network(n) - actor.start_network_loop(n.name, "Waiter") - - env.run(until=2) - expected = [ - "0.0 >> Actor one in Waiter", - "0.0 >> Actor two in Waiter", - "1.0 >> Actor one in Thinker", - "1.0 >> Actor two in Thinker", - "1.0 >> Actor one in Waiter", - "1.0 >> Actor two in Runner", - ] - assert data == expected diff --git a/src/upstage_des/test/test_units.py b/src/upstage_des/test/test_units.py deleted file mode 100644 index 7045e90..0000000 --- a/src/upstage_des/test/test_units.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -from itertools import combinations - -import pytest - -from upstage_des.units.convert import CONVERSIONS, unit_convert - - -def test_convert_fail() -> None: - with pytest.raises(ValueError): - unit_convert(100, "parsec", "km") - - -def test_convert() -> None: - result = unit_convert(100, "km", "mi") - assert result == 0.62137119223 * 100 - - result = unit_convert(3600, "s", "hr") - assert result == 1.0 - - result = unit_convert(10, "min", "s") - assert result == 10.0 * 60.0 - - -def test_convert_reverse() -> None: - for unit_1, unit_2 in combinations(CONVERSIONS, 2): - if unit_2 in CONVERSIONS[unit_1]: - ans = unit_convert(1.0, unit_1, unit_2) - reverse = unit_convert(ans, unit_2, unit_1) - assert pytest.approx(reverse) == 1 diff --git a/src/upstage_des/type_help.py b/src/upstage_des/type_help.py deleted file mode 100644 index fc7c1c2..0000000 --- a/src/upstage_des/type_help.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Help for typing task and simpy generators.""" - -from upstage_des.base import SIMPY_GEN -from upstage_des.routines import ROUTINE_GEN -from upstage_des.task import TASK_GEN - -__all__ = [ - "SIMPY_GEN", - "ROUTINE_GEN", - "TASK_GEN", -] diff --git a/src/upstage_des/units/__init__.py b/src/upstage_des/units/__init__.py deleted file mode 100644 index 6f668e9..0000000 --- a/src/upstage_des/units/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Imports for units.""" - -from .convert import unit_convert - -__all__ = ["unit_convert"] diff --git a/src/upstage_des/units/convert.py b/src/upstage_des/units/convert.py deleted file mode 100644 index 9c51b0c..0000000 --- a/src/upstage_des/units/convert.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. -"""Conversions for common distance and time units.""" - -CONVERSIONS: dict[str, dict[str, float]] = { - "m": { - "km": 1 / 1000.0, - "mi": 1 / 1000.0 * 0.62137119223, - "nmi": 1 / 1000.0 * 0.539957, - "m": 1.0, - }, - "km": {"km": 1.0, "mi": 0.62137119223, "nmi": 0.539957, "m": 1000.0}, - "mi": { - "km": 1 / 0.62137119223, - "mi": 1.0, - "nmi": 0.868976, - "m": 1 / 0.62137119223 * 1000, - }, - "nmi": { - "km": 1 / 0.539957, - "mi": 1 / 0.868976, - "nmi": 1.0, - "m": 1 / 0.539957 * 1000, - }, - "s": {"hr": 1.0 / (60.0 * 60.0), "min": 1.0 / 60.0, "s": 1.0}, - "min": {"hr": 1.0 / 60.0, "min": 1.0, "s": 60.0}, - "hr": {"hr": 1.0, "min": 60.0, "s": (60.0 * 60.0)}, - "day": {"hr": 24.0, "week": 1 / 7.0, "min": 24 * 60.0, "s": 24 * 3600}, - "week": {"day": 7.0, "hr": 7.0 * 24, "min": 7.0 * 24 * 60.0, "s": 7.0 * 24 * 3600}, -} - -DISTANCE_UNITS = ["m", "km", "mi", "nmi", "ft"] -TIME_UNITS = ["s", "min", "hr", "day", "week"] -TIME_ALTERNATES = { - "seconds": "s", - "second": "s", - "minute": "min", - "minutes": "min", - "hour": "hr", - "hours": "hr", - "days": "day", - "weeks": "week", -} -STANDARD_TIMES = ["s", "min", "hr"] - -# add feet to conversions -CONVERSIONS["ft"] = {"ft": 1.0, "mi": 1 / 5280.0} -for unit in DISTANCE_UNITS: - if unit == "ft": - continue - CONVERSIONS["ft"][unit] = CONVERSIONS["mi"][unit] / 5280.0 - CONVERSIONS[unit]["ft"] = 1 / CONVERSIONS["ft"][unit] - - -def unit_convert(value: int | float, units_from: str, units_to: str) -> float: - """Convert between units of distance and time. - - Units must be one of: - distance: km, m, mi, nmi, ft - - time: - - * s, second, or seconds - * min, minute, or minutes - * hr, hour, or hours - * day or days - * week or weeks - - All units are lower-cased on input. - - Args: - value (int | float): Value of "from" unit - units_from (str): Unit to convert from - units_to (str): Unit to convert to - - Raises: - ValueError: _description_ - - Returns: - float: _description_ - """ - units_fr = units_from.lower() - units_t = units_to.lower() - units_fr = TIME_ALTERNATES.get(units_fr, units_fr) - units_t = TIME_ALTERNATES.get(units_t, units_t) - try: - return value * CONVERSIONS[units_fr][units_t] - except KeyError: - raise ValueError(f"Cannot convert from {units_from} to {units_to}.") diff --git a/src/upstage_des/utils.py b/src/upstage_des/utils.py deleted file mode 100644 index a1f51d7..0000000 --- a/src/upstage_des/utils.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (C) 2025 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""This module contains utility functions. - -Note: - Some of the functions included in this module directly support UPSTAGE's - other modules, and some are there for the user's convenience. - -""" - -import inspect -from collections.abc import Sequence -from sys import _getframe as get_frame # pylint: disable=protected-access -from typing import Any, TypeVar - -from .data_types import Location - -__all__ = ( - "debug_assert", - "debug_pause", - "get_caller_info", -) - - -SKIP: bool = True - - -def debug_assert(test: bool, msg: str = "") -> None: - """Coalesces breakpoints for any failing assert to a single line. - - Coalesces all potential lines where the code may fail into one - single line that can be marked as a breakpoint. - - Args: - test (bool): The boolean statement to test, i.e., must evaluate to ``true`` or ``false``. - msg (str): The message to display if the test is false. - - Note: - This is necessary because ``pdb`` sometimes does not work well when - running ``simpy`` due to the way that ``simpy`` handles exceptions. - - This is also helpful when debugging complex behaviors that run the same - code multiple times. Instead of manually writing a ``try/except`` - statement, you can use ``debug_assert`` to do that for you, and ignore - all of them from a single control point. - - - Example: - >>> # 1. Add a ``debug_assert`` statement in your code, e.g.: - >>> from upstage.utils import debug_assert - >>> ... - >>> bar, foo = 0, 1 # <<< change foo to be less than bar to raise - >>> debug_assert(foo > bar, "foo is not greater than bar") - >>> # 2. Put a break point on the ``raise error`` line to see why the assert failed. - - """ - if SKIP: - return - - try: - assert test, msg - except AssertionError as error: - raise error # <<< ADD BREAKPOINT HERE - - -def debug_pause(test: bool | None = None) -> None: - """Call function to pause IDE on debug mode with single breakpoint. - - A helper function to pause the execution of the interpreter when running - the code from an IDE (e.g., PyCharm). - - Args: - test (bool, optional): A boolean statement to pause on when it evaluates to ``true``. - - Note: - This is necessary because ``pdb`` sometimes does not work well when - running ``simpy`` due to the way that ``simpy`` handles exceptions. - - Note: - Put a break point on the ``pass`` line to pause the IDE. - - """ - if SKIP: - return - - if test is not None and test: - pass # <<< ADD BREAKPOINT HERE - - -def get_caller_object(caller_level: int = 2) -> Any: - """Inspect the stack to see who called you. - - Args: - caller_level (int, optional): Number of hops up in the stack. Defaults to 2. - - Returns: - Any: The task object - """ - try: - task_frame = inspect.stack()[caller_level] - task_object = task_frame.frame.f_locals["self"] - return task_object - except Exception: - return None - - -def get_caller_info(caller_level: int = 1) -> str: - """Get information from the object that called the function. - - Parameters - ---------- - caller_level : str, optional - The number of frames to go back in the call stack. - - """ - try: - frame = get_frame(caller_level + 1) - if frame.f_code.co_name == "task": - try: - caller = frame.f_code.co_qualname - if caller: - return caller - except (AttributeError, IndexError): - ... - return frame.f_code.co_name - except ValueError as exc: - if any("call stack is not deep enough" in arg for arg in exc.args): - return "Unknown caller" - raise - except Exception: - raise - - -T = TypeVar("T") - - -def iterable_convert(item: T | list[T] | tuple[T, ...]) -> list[T]: - """Convert single objects or tuples into a list. - - Args: - item (T | list[T] | tuple[T,...]): Object, list, or tuple to convert. - - Returns: - list[T]: The list version of the input. - """ - if not isinstance(item, list | tuple): - return [item] - return list(item) - - -def waypoint_time_and_dist( - start: Location, - waypoints: Sequence[Location], - speed: float, -) -> tuple[float, float]: - """Get the time and distance of a series of locations. - - Args: - start (Location): Starting point - waypoints (Sequence[Location]): Waypoints after the start - speed (float): Travel speed - - Returns: - tuple[float, float]: Time and Distance over the waypoints. - """ - dist = 0.0 - current = start - for wypt in waypoints: - dist += wypt - current - current = wypt - time = dist / speed - return time, dist From 40504f084afe4819502cd5e3d80eae1ca3dc4ff6 Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 17:24:06 -0400 Subject: [PATCH 02/12] Initial commit of version 1.0. Actors as dataclass-like objects. Basic states and events. Tasks. --- README.md | 68 +- pixi.lock | 1191 +----------------------- pixi.toml | 16 +- pyproject.toml | 73 +- src/upstage_des/__init__.py | 70 ++ src/upstage_des/actor.py | 544 +++++++++++ src/upstage_des/base.py | 468 ++++++++++ src/upstage_des/cleanup.py | 17 + src/upstage_des/data_utils/__init__.py | 6 + src/upstage_des/events.py | 624 +++++++++++++ src/upstage_des/geography/__init__.py | 6 + src/upstage_des/py.typed | 0 src/upstage_des/rehearsing.py | 8 + src/upstage_des/resources/__init__.py | 6 + src/upstage_des/root_types.py | 22 + src/upstage_des/states.py | 403 ++++++++ src/upstage_des/tasks.py | 315 +++++++ src/upstage_des/units.py | 92 ++ tests/test_active_state.py | 239 +++++ tests/test_actor_clone.py | 145 +++ tests/test_actor_state.py | 174 ++++ tests/test_entity_registry.py | 117 +++ tests/test_events.py | 558 +++++++++++ tests/test_knowledge.py | 58 ++ tests/test_stage.py | 422 +++++++++ tests/test_tasks.py | 410 ++++++++ tests/test_units.py | 32 + 27 files changed, 4773 insertions(+), 1311 deletions(-) create mode 100644 src/upstage_des/__init__.py create mode 100644 src/upstage_des/actor.py create mode 100644 src/upstage_des/base.py create mode 100644 src/upstage_des/cleanup.py create mode 100644 src/upstage_des/data_utils/__init__.py create mode 100644 src/upstage_des/events.py create mode 100644 src/upstage_des/geography/__init__.py create mode 100644 src/upstage_des/py.typed create mode 100644 src/upstage_des/rehearsing.py create mode 100644 src/upstage_des/resources/__init__.py create mode 100644 src/upstage_des/root_types.py create mode 100644 src/upstage_des/states.py create mode 100644 src/upstage_des/tasks.py create mode 100644 src/upstage_des/units.py create mode 100644 tests/test_active_state.py create mode 100644 tests/test_actor_clone.py create mode 100644 tests/test_actor_state.py create mode 100644 tests/test_entity_registry.py create mode 100644 tests/test_events.py create mode 100644 tests/test_knowledge.py create mode 100644 tests/test_stage.py create mode 100644 tests/test_tasks.py create mode 100644 tests/test_units.py diff --git a/README.md b/README.md index b450786..00cdb13 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,3 @@ -# UPSTAGE +# upstage_des -UPSTAGE is a **U**niversal **P**latform for **S**imulating -**T**asks and **A**ctors with **G**raphs and **E**vents built atop -[__`SimPy 4`__][simpy-repo]. - -## ✨ Try it in your browser ✨ - -➡️ **https://gtri.github.io/upstage/demo** - -## What is UPSTAGE for? - -__UPSTAGE__ is a Python framework for creating robust, behavior-driven Discrete Event Simulations (DES). The primary goal of UPSTAGE is to enable the quick creation of simulations at any desired level of abstraction with built-in data recording, simulation integrity and runtime checks, and assistance for the usual pitfalls in custom discrete-event simulation: interrupts and cancellations. It is designed is to simplify the development process for simulation models of *complex systems of systems*. - -__UPSTAGE__ leverages the extensible [__`SimPy`__][simpy-docs] library and adds two concepts to accelerate the generation of complex discrete-event simulations. - -1. `Actor` - i.e., an entity that has `State` -2. `Task` - i.e., actions actors can perform and that can be organized into a `TaskNetwork`. - -Actors can have multiple networks running on them, their states can be shared, and there are features for interactions between task networks running on the same actor. Those tasks modify the states on their actor, with features for real-time states that update on request without requiring time-stepping or modifying the existing events. - -![image](docs/source/_static/upstage-flow.png) - -Additional features include: - -1. Context-aware `EnvironmentContext`, accessed via `UpstageBase`, enabling thread-safe simulation globals for the _Stage_ and _Named Entities_ (see below). -1. __Active States__ (e.g.,`LinearChangingState`) represent continuous-time attributes of actors that can be queried at discrete points in time, or trigger events when they reach a certain level. -1. Spatial-aware data types (e.g., `CartesianLocation`) and states like the waypoint-following `GeodeticLocationChangingState`. -1. Geodetic and cartesian positions, distances, and motion - with ranged sensing. -1. `NamedEntity` in a thread-safe global context, enabling easier "director" logic creation with less argument passing in your code -1. The `Stage`: a global context variable for simulation properties and attributes. This enables under-the-hood coordination of motion, geography, and other features. -1. __Rehearsal__: Write planning and simulation code in one place only, and "rehearse" an actor through a task network using planning factors to discover task feasibility before the actor attempts to complete the task. -1. All States are recordable, and some record dataclass and dictionary values -1. A `Routine` class for building reusable event behaviors to simplify `Task` coding. -1. Point-To-Point and Routing Table communications handlers. -1. Numerous runtime checks and error handling for typical DES pitfalls: based on more than a decade of custom DES-building experience. -1. And more! - -See the [documentation][upstage-docs] for tutorials and details. - -## Requirements - -UPSTAGE only requires Python 3.11+ and Simpy 4+. - -## Installation - -In an environment (Python 3.11+) of your choice: - -```console -pip install upstage-des -``` - -## Documentation - -See the [documentation][upstage-docs] for tutorials and additional details. - -## How do I contribute or set up a develpment environment? - -See [CONTRIBUTING][contributing] for instructions on setting up an environment and contributing. - -For information on how to style your code, see the [Style Guide][style-guide]. - -[contributing]: ./CONTRIBUTING.md -[style-guide]: ./STYLE_GUIDE.md -[simpy-docs]: https://simpy.readthedocs.io/en/latest/ -[simpy-repo]: https://gitlab.com/team-simpy/simpy/ -[upstage-docs]: https://gtri.github.io/upstage +This is a software library for doing discrete event simulation. diff --git a/pixi.lock b/pixi.lock index 689230d..c19d5b2 100644 --- a/pixi.lock +++ b/pixi.lock @@ -844,382 +844,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313ha7868ed_1.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ - py311: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hfdbb021_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py311hf29c0ef_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py311h2dc5d0c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.11-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.7-h0d44e9d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py311hcfaa980_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py311h2dc5d0c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py311h9ecbd09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py311h9ecbd09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.5.1-py39h77e2912_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.11-h9e4cc4f_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.11-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-5_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py311h2dc5d0c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py311hb02d549_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py311h9ecbd09_1.conda - - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - - pypi: ./ - osx-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py311hd89902b_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py311h137bacd_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py311ha3cf9ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.11-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-20.1.1-hf95d169_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h4b5e92a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.7-hebb159f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.39-h03b04e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py311h284d43a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py311ha3cf9ac_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py311h4d7f069_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py311h4d7f069_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.5.1-py39h286ba15_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.11-h9ccd52b_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.11-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.11-5_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py311ha3cf9ac_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py311hf416f03_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py311h4d7f069_1.conda - - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - - pypi: ./ - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py311h3f08180_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py311h3a79f62_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py311h4921393_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.1-ha82da77_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.7-h178c5d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.39-h223e5b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py311h0d6554d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py311h4921393_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py311h917b07b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py311h917b07b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py311hd3675e7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.11-hc22306f_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.11-5_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py311h4921393_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py311h92c7caa_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311h917b07b_1.conda - - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - - pypi: ./ - win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py311hda3d55a_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py311he736701_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py311h5082efb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.11-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.7-he286e8c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py311hf779c20_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py311h5082efb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py311he736701_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py311he736701_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.5.1-py39he870945_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.11-h3f84c4b_2_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.11-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.11-5_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py311h5082efb_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py311h0e48851_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.42.34438-h7142326_25.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py311he736701_1.conda - - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - - pypi: ./ py312: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -2645,23 +2269,6 @@ packages: purls: [] size: 4213 timestamp: 1737382993425 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hfdbb021_2.conda - sha256: 949913bbd1f74d1af202d3e4bff2e0a4e792ec00271dc4dd08641d4221aa2e12 - md5: d21daab070d76490cb39a8f1d1729d79 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - constrains: - - libbrotlicommon 1.1.0 hb9d3cd8_2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 350367 - timestamp: 1725267768486 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_2.conda sha256: f2a59ccd20b4816dea9a2a5cb917eb69728271dbf1aeab4e1b7e609330a50b6f md5: b0b867af6fc74b2a0aa206da29c0f3cf @@ -2696,22 +2303,6 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 350424 timestamp: 1725267803672 -- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py311hd89902b_2.conda - sha256: 004cefbd18f581636a8dcb1964fb73478f15d496769226ec896c1d4a0161b7d8 - md5: d75f06ee06001794aa83a05e885f1520 - depends: - - __osx >=10.13 - - libcxx >=17 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - constrains: - - libbrotlicommon 1.1.0 h00291cd_2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 363793 - timestamp: 1725267947069 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312h5861a67_2.conda sha256: 265764ff4ad9e5cfefe7ea85c53d95157bf16ac2c0e5f190c528e4c9c0c1e2d0 md5: b95025822e43128835826ec0cc45a551 @@ -2744,23 +2335,6 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 363156 timestamp: 1725268004102 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py311h3f08180_2.conda - sha256: f507d65e740777a629ceacb062c768829ab76fde01446b191699a734521ecaad - md5: c8793a23206344faa25f4e0b5d0e7908 - depends: - - __osx >=11.0 - - libcxx >=17 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - constrains: - - libbrotlicommon 1.1.0 hd74edd7_2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 339584 - timestamp: 1725268241628 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hde4cb15_2.conda sha256: 254b411fa78ccc226f42daf606772972466f93e9bc6895eabb4cfda22f5178af md5: a83c2ef76ccb11bc2349f4f17696b15d @@ -2795,23 +2369,6 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 339067 timestamp: 1725268603536 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py311hda3d55a_2.conda - sha256: aa3ac5dbf63db2f145235708973c626c2189ee4040d769fdf0076286fa45dc26 - md5: a0ea2839841a06740a1c110ba3317b42 - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - libbrotlicommon 1.1.0 h2466b09_2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 322114 - timestamp: 1725268368720 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_2.conda sha256: f83baa6f6bcba7b73f6921d5c1aa95ffc5d8b246ade933ade79250de0a4c9c4c md5: a99aec1ac46794a5fb1cd3cf5d2b6110 @@ -2949,22 +2506,6 @@ packages: - pkg:pypi/certifi?source=compressed-mapping size: 162721 timestamp: 1739515973129 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py311hf29c0ef_0.conda - sha256: bc47aa39c8254e9e487b8bcd74cfa3b4a3de3648869eb1a0b89905986b668e35 - md5: 55553ecd5328336368db611f350b7039 - depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 302115 - timestamp: 1725560701719 - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda sha256: cba6ea83c4b0b4f5b5dc59cb19830519b28f95d7ebef7c9c5cf1c14843621457 md5: a861504bbea4161a9170b85d4d2be840 @@ -2997,21 +2538,6 @@ packages: - pkg:pypi/cffi?source=hash-mapping size: 295514 timestamp: 1725560706794 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py311h137bacd_0.conda - sha256: 012ee7b1ed4f9b0490d6e90c72decf148d7575173c7eaf851cd87fd434d2cacc - md5: a4b0f531064fa3dd5e3afbb782ea2cd5 - depends: - - __osx >=10.13 - - libffi >=3.4,<4.0a0 - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 288762 - timestamp: 1725560945833 - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py312hf857d28_0.conda sha256: 94fe49aed25d84997e2630d6e776a75ee2a85bd64f258702c57faa4fe2986902 md5: 5bbc69b8194fedc2792e451026cac34f @@ -3042,22 +2568,6 @@ packages: - pkg:pypi/cffi?source=hash-mapping size: 284540 timestamp: 1725560667915 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py311h3a79f62_0.conda - sha256: 253605b305cc4548b8f97eb7c2e146697e0c7672b099c4862ec5ca7e8e995307 - md5: a42272c5dbb6ffbc1a5af70f24c7b448 - depends: - - __osx >=11.0 - - libffi >=3.4,<4.0a0 - - pycparser - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 288211 - timestamp: 1725560745212 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda sha256: 8d91a0d01358b5c3f20297c6c536c5d24ccd3e0c2ddd37f9d0593d0f0070226f md5: 19a5456f72f505881ba493979777b24e @@ -3090,22 +2600,6 @@ packages: - pkg:pypi/cffi?source=hash-mapping size: 282115 timestamp: 1725560759157 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py311he736701_0.conda - sha256: 9689fbd8a31fdf273f826601e90146006f6631619767a67955048c7ad7798a1d - md5: e1c69be23bd05471a6c623e91680ad59 - depends: - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 297627 - timestamp: 1725561079708 - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py312h4389bb4_0.conda sha256: ac007bf5fd56d13e16d95eea036433012f2e079dc015505c8a79efebbad1fcbc md5: 08310c1a22ef957d537e547f8d484f92 @@ -3183,21 +2677,6 @@ packages: - pkg:pypi/comm?source=hash-mapping size: 12103 timestamp: 1733503053903 -- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py311h2dc5d0c_0.conda - sha256: 88eceeaed558d6b313564142a6c013646cbd5289d5f20a61253bcdfe198e6f32 - md5: 5f57c67f3880dd62b83b3867ea03d9bc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - tomli - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/coverage?source=compressed-mapping - size: 381831 - timestamp: 1742591861830 - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py312h178313f_0.conda sha256: 5c502e6a72f46af9e6dd74e9d91449898c72ccb36dad46db7a09101042eef0c2 md5: c9c5941aa3ad8c8324edf65128121395 @@ -3228,20 +2707,6 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 378570 timestamp: 1742591809856 -- conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py311ha3cf9ac_0.conda - sha256: 495ea8caa559fbe5cceabc181219ea22de06fc26a3878ef8ab876e9ff99fe54a - md5: 588d993196273d5a122d24643751c39b - depends: - - __osx >=10.13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - tomli - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/coverage?source=hash-mapping - size: 380754 - timestamp: 1742591916914 - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py312h3520af0_0.conda sha256: 0153ea1d55d1c4a88a9a7e6ba24332cdac3379c2e62874d7af91e8586606ede0 md5: 3aac48d735f438a582078c623c1f6a70 @@ -3270,21 +2735,6 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 377981 timestamp: 1742591939877 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py311h4921393_0.conda - sha256: 9af78df87955d068231e8a436041f8cc4ec4e559ab59697037eb183cba0c1435 - md5: 1ba342dd65d19f88074b07c773cbb0e9 - depends: - - __osx >=11.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - - tomli - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/coverage?source=hash-mapping - size: 381697 - timestamp: 1742591894581 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py312h998013c_0.conda sha256: 9da0faf9092eea8ae195b0316c039a1e59a9e10c43d4a16660b70341515b15bb md5: 07426bb994e08ea760003047e7e6f68f @@ -3315,22 +2765,6 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 378772 timestamp: 1742591852148 -- conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py311h5082efb_0.conda - sha256: 090a887e578c4dacadf91095ce1b5e6795ee788cbf4d92b04c1aeaf3874676e3 - md5: 0c26e60d5e208cd3161331d9ad84dee1 - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - tomli - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/coverage?source=hash-mapping - size: 407319 - timestamp: 1742592102065 - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py312h31fea79_0.conda sha256: 135bd1283053bb0f83f5166d17a13cc9c73c6d8af2d3e09f57d360a81413f0af md5: b2d00351ff17283c886c65f1121c21a4 @@ -3363,17 +2797,6 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 403906 timestamp: 1742592209260 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.11-py311hd8ed1ab_2.conda - noarch: generic - sha256: 52e462716ff6b062bf6992f9e95fcb65a0b95a47db73f0478bd0ceab8a37036a - md5: fb7bc3f1bccb39021a53309e83bce28d - depends: - - python 3.11.11.* - - python_abi * *_cp311 - license: Python-2.0 - purls: [] - size: 46889 - timestamp: 1741034069952 - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda noarch: generic sha256: 58a637bc8328b115c9619de3fcd664ec26662083319e3c106917a1b3ee4d7594 @@ -4913,22 +4336,6 @@ packages: purls: [] size: 55476 timestamp: 1727963768015 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py311hcfaa980_0.conda - sha256: b4610371f579b85680e11ea13568826f8ac7d453deec86e40ea6b448935ef70c - md5: b951c99c1d51236ae9f72ae54b7ec63b - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause and MIT-CMU - purls: - - pkg:pypi/lxml?source=hash-mapping - size: 1390869 - timestamp: 1739211738608 - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py312he28fd5a_0.conda sha256: 4f3a78b59890f2175a381d9ae5e74b4523aea23daaa01cafbb150456bc8b857c md5: 52d16dd592060d4b2fa9ad325e0c1f90 @@ -4961,21 +4368,6 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1404411 timestamp: 1739211853813 -- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py311h284d43a_0.conda - sha256: 58690c1992101a0a2ff34e819233707575075c1066a3890bc3ea2cda6c02f411 - md5: 7a0ef998f9d116ee74f042e309a041b4 - depends: - - __osx >=10.13 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause and MIT-CMU - purls: - - pkg:pypi/lxml?source=hash-mapping - size: 1236231 - timestamp: 1739211987438 - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py312h91b2f42_0.conda sha256: 9d8caf0b13e42a214f47f21e4a4696a7de37e81b182fb88c0e922b5940fb716e md5: a1b3a0206fc6c434fadc25d818002b82 @@ -5006,22 +4398,6 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1258398 timestamp: 1739212205592 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py311h0d6554d_0.conda - sha256: a2bcff7d15afe8cd2d5e3f5d60a982622a2ff98ca0ae95c184cbba24fbb594d2 - md5: babfe696c09e05de2333fa7acf6b52b8 - depends: - - __osx >=11.0 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause and MIT-CMU - purls: - - pkg:pypi/lxml?source=hash-mapping - size: 1198659 - timestamp: 1739211973305 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py312h9535dd2_0.conda sha256: b899871ecf3f331e3047295897809758a02a144e4118f1378ca443c62772cd2c md5: f9d4307bbe7d394ac3634fe85a4c0e94 @@ -5054,23 +4430,6 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1219666 timestamp: 1739211889959 -- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py311hf779c20_0.conda - sha256: fef2a03de0ee414876aa09951e83abb3e87ac6fcc344a9c97cec74c4bb86de60 - md5: db5e936c50281b792269dd56e8e78782 - depends: - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause and MIT-CMU - purls: - - pkg:pypi/lxml?source=hash-mapping - size: 1057157 - timestamp: 1739212180275 - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py312h53bce91_0.conda sha256: 78519f3a92e8e284792b9b13d4240643b47b3c1902b2288e2a4dfeb83f78e787 md5: c86f153c26b4d6235de9e19eafc01ce8 @@ -5117,22 +4476,6 @@ packages: - pkg:pypi/markdown-it-py?source=hash-mapping size: 64430 timestamp: 1733250550053 -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py311h2dc5d0c_1.conda - sha256: 0291d90706ac6d3eea73e66cd290ef6d805da3fad388d1d476b8536ec92ca9a8 - md5: 6565a715337ae279e351d0abd8ffe88a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 25354 - timestamp: 1733219879408 - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda sha256: 4a6bf68d2a2b669fecc9a4a009abd1cf8e72c2289522ff00d81b5a6e51ae78f5 md5: eb227c3e0bf58f5bd69c0532b157975b @@ -5165,21 +4508,6 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24856 timestamp: 1733219782830 -- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py311ha3cf9ac_1.conda - sha256: e9965b5d4c29b17b1512035b24a7c126ed7bdb6b39103b52cae099d5bb4194a9 - md5: 1d6596ca7c7b66215c5c0d58b3cb0dd3 - depends: - - __osx >=10.13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 24688 - timestamp: 1733219887972 - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py312h3520af0_1.conda sha256: d521e272f7789ca62e7617058a4ea3bd79efa73de1a39732df209ca5299e64e2 md5: 32d6bc2407685d7e2d8db424f42018c6 @@ -5210,22 +4538,6 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24363 timestamp: 1733219815199 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py311h4921393_1.conda - sha256: 4f738a7c80e34e5e5d558e946b06d08e7c40e3cc4bdf08140bf782c359845501 - md5: 249e2f6f5393bb6b36b3d3a3eebdcdf9 - depends: - - __osx >=11.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 24976 - timestamp: 1733219849253 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda sha256: 4aa997b244014d3707eeef54ab0ee497d12c0d0d184018960cce096169758283 md5: 46e547061080fddf9cf95a0327e8aba6 @@ -5258,23 +4570,6 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24757 timestamp: 1733219916634 -- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py311h5082efb_1.conda - sha256: 6f756e13ccf1a521d3960bd3cadddf564e013e210eaeced411c5259f070da08e - md5: c1f2ddad665323278952a453912dc3bd - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 28238 - timestamp: 1733220208800 - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda sha256: bbb9595fe72231a8fbc8909cfa479af93741ecd2d28dfe37f8f205fef5df2217 md5: 944fdd848abfbd6929e57c790b8174dd @@ -5356,24 +4651,7 @@ packages: purls: - pkg:pypi/mistune?source=hash-mapping size: 72749 - timestamp: 1742402716323 -- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py311h9ecbd09_0.conda - sha256: 5bed33e02328bc0b3fbbf39c201c297ad6051d4d2c72415f2fdb9b7152279185 - md5: 51d9f9d088f232de3648ddefd559cddc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mypy?source=hash-mapping - size: 18359703 - timestamp: 1738768552907 + timestamp: 1742402716323 - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py312h66e93f0_0.conda sha256: b57c8bd233087479c70cb3ee3420861e0625b8a5a697f5abe41f5103fb2c2e69 md5: a84061bc7e166712deb33bf7b32f756d @@ -5408,22 +4686,6 @@ packages: - pkg:pypi/mypy?source=hash-mapping size: 17058016 timestamp: 1738767732637 -- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py311h4d7f069_0.conda - sha256: 94ff2be54745d42d9429fa06594c9c66e563e1d818d4979ec9fcede2448f4be2 - md5: 8a0036307a3c2356b95c76e0360e2b4f - depends: - - __osx >=10.13 - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mypy?source=hash-mapping - size: 12315897 - timestamp: 1738768112915 - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py312h01d7ebd_0.conda sha256: 38132c4b5de6686965f21b51a1656438e83b2a53d6f50e9589e73fb57a43dd49 md5: 0251bb4d6702b729b06fd5c7918e9242 @@ -5456,23 +4718,6 @@ packages: - pkg:pypi/mypy?source=hash-mapping size: 11022410 timestamp: 1738768159908 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py311h917b07b_0.conda - sha256: 7ed54f9988070ce12de61c9f0a7d1fa9c4d7933b847e16b2efebd5360e069559 - md5: 4983e0d4dbeeca83f255938ff92cd8cb - depends: - - __osx >=11.0 - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mypy?source=hash-mapping - size: 9779580 - timestamp: 1738768242703 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py312hea69d52_0.conda sha256: 7284d77173d385f5c7456c13d825dbae170920a31ca7a0996d2608ad17f17e2f md5: 909034322685579577b1bbb9b47e39e1 @@ -5507,24 +4752,6 @@ packages: - pkg:pypi/mypy?source=compressed-mapping size: 10275919 timestamp: 1738768578918 -- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py311he736701_0.conda - sha256: 5142b091f218599b44ab662ec687d8eacfc880fa40a90116d4ed14232ae60bc9 - md5: 8ae5328f0a002251430cb38684efb7fd - depends: - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mypy?source=hash-mapping - size: 10113574 - timestamp: 1738767838546 - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py312h4389bb4_0.conda sha256: 3bab35d2f17f9b2c8498c952f7d182848f2d70775e7e970d5f53c7eeb87741a6 md5: 1eea4f4c0038b6f9b399dfad2305cd6f @@ -5894,20 +5121,6 @@ packages: - pkg:pypi/prompt-toolkit?source=compressed-mapping size: 271841 timestamp: 1744724188108 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py311h9ecbd09_0.conda - sha256: 50d0944b59a9c6dfa6b99cc2632bf8bc9bef9c7c93710390ded6eac953f0182d - md5: 1a390a54b2752169f5ba4ada5a8108e4 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 484778 - timestamp: 1740663319335 - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda sha256: 158047d7a80e588c846437566d0df64cec5b0284c7184ceb4f3c540271406888 md5: 8e30db4239508a538e4a3b3cdf5b9616 @@ -5936,19 +5149,6 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 475101 timestamp: 1740663284505 -- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py311h4d7f069_0.conda - sha256: e290563f61f810f745b32d4c1ebe4ec87827323134f6bee2e8cc894391cbc548 - md5: 7b5cdf63ced6576ead40a82ea0616322 - depends: - - __osx >=10.13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 490169 - timestamp: 1740663371249 - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py312h01d7ebd_0.conda sha256: bdfa40a1ef3a80c3bec425a5ed507ebda2bdebce2a19bccb000db9d5c931750c md5: fcad6b89f4f7faa999fa4d887eab14ba @@ -5975,20 +5175,6 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 482494 timestamp: 1740663492867 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py311h917b07b_0.conda - sha256: 3ea107f769b3ac99411f6bd6d86f946566ba3983894cbeb0e43439934a90c2f5 - md5: 12f8d65fb5a6bd03aedd5ac74391f1ea - depends: - - __osx >=11.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 492006 - timestamp: 1740663355030 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda sha256: cb11dcb39b2035ef42c3df89b5a288744b5dcb5a98fb47385760843b1d4df046 md5: 0f461bd37cb428dc20213a08766bb25d @@ -6017,21 +5203,6 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 484139 timestamp: 1740663381126 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py311he736701_0.conda - sha256: e3844e26821651f744ea57a1538a8f970872f15a1c6eb38fc208f0efd1c3706c - md5: fc2a628caa77146532ee4747894bccd5 - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 499375 - timestamp: 1740663711326 - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda sha256: 088451ee2c9a349e1168f70afe275e58f86350faffb09c032cff76f97d4fb7bb md5: f5b86d6e2e645ee276febe79a310b640 @@ -6255,23 +5426,6 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 1504931 timestamp: 1740150987346 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py311hd3675e7_0.conda - sha256: b07b808f8124b9642e5b38f2fabe7136f7166c3a108a0b6d7c483b4db6e16c7a - md5: 47663abd5b3d3a5f99fa61bfd56111de - depends: - - __osx >=11.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - - toml-fmt-common 1.0.1 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyproject-fmt?source=hash-mapping - size: 1183346 - timestamp: 1730382509358 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py312h6f6235b_0.conda sha256: 7f3e81bf127c894140bf608f71e3f96d902eeaf2525b766b0f99a04f6371084e md5: f64845e77fa7c6262abd68e190989a7f @@ -6439,34 +5593,6 @@ packages: - pkg:pypi/pytest-xdist?source=hash-mapping size: 38147 timestamp: 1733240891538 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.11-h9e4cc4f_2_cpython.conda - build_number: 2 - sha256: e0be7ad95a034d10e021f15317bf5c70fc1161564fa47844984c245505cde36c - md5: 81dd3e521f9b9eaa58d06213e28aaa9b - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 - - liblzma >=5.6.4,<6.0a0 - - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.49.1,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libxcrypt >=4.4.36 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 - purls: [] - size: 30594389 - timestamp: 1741036299726 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda build_number: 1 sha256: 77f2073889d4c91a57bc0da73a0466d9164dbcf6191ea9c3a7be6872f784d625 @@ -6522,29 +5648,6 @@ packages: size: 33233150 timestamp: 1739803603242 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.11-h9ccd52b_2_cpython.conda - build_number: 2 - sha256: 2c34d988cdb364665478ca3d93a43b2a5bf149e822215ad3fa6a5342627374a9 - md5: 8d73135b48597cc13715a34bc79654b7 - depends: - - __osx >=10.13 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 - - libsqlite >=3.49.1,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 - purls: [] - size: 15472260 - timestamp: 1741035097532 - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.9-h9ccd52b_1_cpython.conda build_number: 1 sha256: c394f7068a714cad7853992f18292bb34c6d99fe7c21025664b05069c86b9450 @@ -6592,29 +5695,6 @@ packages: size: 13961675 timestamp: 1739802065430 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.11-hc22306f_2_cpython.conda - build_number: 2 - sha256: 6f3c20b8666301fc27e6d1095f1e0f12a093bacf483e992cb56169127e989630 - md5: 4bd51247ba4dd5958eb8f1e593edfe00 - depends: - - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 - - libsqlite >=3.49.1,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 - purls: [] - size: 14579450 - timestamp: 1741035010673 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.9-hc22306f_1_cpython.conda build_number: 1 sha256: fe804fc462396baab8abe525a722d0254c839533c98c47abd2c6d1248ad45e93 @@ -6662,29 +5742,6 @@ packages: size: 11682568 timestamp: 1739801342527 python_site_packages_path: lib/python3.13/site-packages -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.11-h3f84c4b_2_cpython.conda - build_number: 2 - sha256: d9a31998083225dcbef7c10cf0d379b1f64176cf1d0f8ad7f29941d2eb293d25 - md5: 8959f363205d55bb6ada26bdfd6ce8c7 - depends: - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 - - libsqlite >=3.49.1,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.1,<4.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 - purls: [] - size: 18221686 - timestamp: 1741034476958 - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.9-h3f84c4b_1_cpython.conda build_number: 1 sha256: 320acd0095442a451c4e0f0f896bed2f52b3b8f05df41774e5b0b433d9fa08e0 @@ -6755,16 +5812,6 @@ packages: - pkg:pypi/fastjsonschema?source=hash-mapping size: 226259 timestamp: 1733236073335 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.11-hd8ed1ab_2.conda - sha256: f6398783819f90cf048161cbb1eac86fdc9d54c6be6a0ff771906be3636979a5 - md5: ce36c654f337283c2738bdc71072ecf8 - depends: - - cpython 3.11.11.* - - python_abi * *_cp311 - license: Python-2.0 - purls: [] - size: 46868 - timestamp: 1741034106048 - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda sha256: d45a5a99ec3ad65d390590905c0d79b6223468d75425d988106473056ddc35e7 md5: a1a3aa64397603a81615400388409e10 @@ -6796,17 +5843,6 @@ packages: - pkg:pypi/python-json-logger?source=hash-mapping size: 13383 timestamp: 1677079727691 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-5_cp311.conda - build_number: 5 - sha256: 2660b8059b3ee854bc5d3c6b1fce946e5bd2fe8fbca7827de2c5885ead6209de - md5: 139a8d40c8a2f430df31048949e450de - constrains: - - python 3.11.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6211 - timestamp: 1723823324668 - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda build_number: 5 sha256: d10e93d759931ffb6372b45d65ff34d95c6000c61a07e298d162a3bc2accebb0 @@ -6829,17 +5865,6 @@ packages: purls: [] size: 6217 timestamp: 1723823393322 -- conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.11-5_cp311.conda - build_number: 5 - sha256: 9b092850a268aca99600b724bae849f51209ecd5628e609b4699debc59ff1945 - md5: e6d62858c06df0be0e6255c753d74787 - constrains: - - python 3.11.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6303 - timestamp: 1723823062672 - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda build_number: 5 sha256: 4da26c7508d5bc5d8621e84dc510284402239df56aab3587a7d217de9d3c806d @@ -6862,17 +5887,6 @@ packages: purls: [] size: 6291 timestamp: 1723823083064 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.11-5_cp311.conda - build_number: 5 - sha256: adc05729b7e0aca7b436e60a86f10822a92185dfcb48d66d6444e3629d3a1f6a - md5: 3b855e3734344134cb56c410f729c340 - constrains: - - python 3.11.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6308 - timestamp: 1723823096865 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda build_number: 5 sha256: 49d624e4b809c799d2bf257b22c23cf3fc4460f5570d9a58e7ad86350aeaa1f4 @@ -6895,17 +5909,6 @@ packages: purls: [] size: 6322 timestamp: 1723823058879 -- conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.11-5_cp311.conda - build_number: 5 - sha256: 9b210e5807dd9c9ed71ff192a95f1872da597ddd10e7cefec93a922fe22e598a - md5: 895b873644c11ccc0ab7dba2d8513ae6 - constrains: - - python 3.11.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6707 - timestamp: 1723823225752 - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-5_cp312.conda build_number: 5 sha256: 9486662af81a219e96d343449eff242f38d7c5128ced5ce5acf85857265058d6 @@ -7000,21 +6003,6 @@ packages: - pkg:pypi/pywinpty?source=hash-mapping size: 217133 timestamp: 1738661059040 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py311h2dc5d0c_2.conda - sha256: d107ad62ed5c62764fba9400f2c423d89adf917d687c7f2e56c3bfed605fb5b3 - md5: 014417753f948da1f70d132b2de573be - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 213136 - timestamp: 1737454846598 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda sha256: 159cba13a93b3fe084a1eb9bda0a07afc9148147647f0d437c3c3da60980503b md5: cf2485f39740de96e2a7f2bb18ed2fee @@ -7045,20 +6033,6 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 205919 timestamp: 1737454783637 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py311ha3cf9ac_2.conda - sha256: 4855c51eedcde05f3d9666a0766050c7cbdff29b150d63c1adc4071637ba61d7 - md5: f49b0da3b1e172263f4f1e2f261a490d - depends: - - __osx >=10.13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 197287 - timestamp: 1737454852180 - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312h3520af0_2.conda sha256: de96d83b805dba03422d39e855fb33cbeedc8827235d6f76407a3b42dc085910 md5: 4a2d83ac55752681d54f781534ddd209 @@ -7087,21 +6061,6 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 196573 timestamp: 1737455046063 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py311h4921393_2.conda - sha256: 2af6006c9f692742181f4aa2e0656eb112981ccb0b420b899d3dd42c881bd72f - md5: 250b2ee8777221153fd2de9c279a7efa - depends: - - __osx >=11.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 196951 - timestamp: 1737454935552 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda sha256: ad225ad24bfd60f7719709791345042c3cb32da1692e62bd463b084cf140e00d md5: 68149ed4d4e9e1c42d2ba1f27f08ca96 @@ -7132,22 +6091,6 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 194243 timestamp: 1737454911892 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py311h5082efb_2.conda - sha256: 6095e1d58c666f6a06c55338df09485eac34c76e43d92121d5786794e195aa4d - md5: e474ba674d780f0fa3b979ae9e81ba91 - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 187430 - timestamp: 1737454904007 - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda sha256: 76fec03ef7e67e37724873e1f805131fb88efb57f19e9a77b4da616068ef5c28 md5: ba00a2e5059c1fde96459858537cc8f5 @@ -7183,7 +6126,6 @@ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.4.0-py312hbf22597_0.conda sha256: 65a264837f189b0c69c5431ea8ef44e405c472fedba145b05055f284f08bc663 md5: fa0ab7d5bee9efbc370e71bcb5da9856 - depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -7477,23 +6419,6 @@ packages: - pkg:pypi/rpds-py?source=hash-mapping size: 252641 timestamp: 1747837734433 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py311hb02d549_0.conda - sha256: 7f760d49a226802efd5497eebed5cbb9401426f6fa84bbdfac1d9538487ae680 - md5: 08039294fec98d2050bffd5d4d1d42bd - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 8884884 - timestamp: 1742584321402 - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py312hf79aa60_0.conda sha256: 72e1934499126cb9a3a5aa00e535fc430617206f0ecd8f34f5afd6bdb572a6a8 md5: ce118d87ae26bd6204ac95aa7d7bd32e @@ -7528,22 +6453,6 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 8872400 timestamp: 1742584319600 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py311hf416f03_0.conda - sha256: ebcec004c372bfad0ef686eebe99ad9009f657d263dcffe013a5b89f80292915 - md5: c8ad60e7671fce9570c467b12ec93c4c - depends: - - __osx >=10.13 - - libcxx >=18 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - constrains: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 8183136 - timestamp: 1742584873619 - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py312ha54e1fc_0.conda sha256: 972a8192ec8f73d20f8e665c4cafd5aeefdc8bd8adbfdb83fc1c2bd02598d3cb md5: dcc943dc0c72dbd74524c0e410243204 @@ -7576,23 +6485,6 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 8171146 timestamp: 1742584829512 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py311h92c7caa_0.conda - sha256: f84c8b14f4dd4b5f47fb40881df573abebf9cebdfa8682d47a5aac845ea52f4d - md5: 63e466f2301c13c96fa42a766398d03a - depends: - - __osx >=11.0 - - libcxx >=18 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 7796862 - timestamp: 1742585308514 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py312h31a5b27_0.conda sha256: d8ec16cdee63ab6279f2f174344563e0eef5597167bfe3b1d4001b5c9f140187 md5: 04650bb002095ba4918311d035c167b2 @@ -7627,21 +6519,6 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 7801806 timestamp: 1742584905314 -- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py311h0e48851_0.conda - sha256: 725e1bf73967b33d3250cadec26d88a8c53686cb72d969fe1fe2634b055f8b14 - md5: 0679684afb472be9bae82f46a94654c6 - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 7913230 - timestamp: 1742585238711 - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py312hc33538c_0.conda sha256: 3444f42e218faa07de917de7aeec08a16a3dba9a855aabe6f72ea721792edc3d md5: ab488dc1f8101c17d0fc4ff938860cec @@ -8183,8 +7060,8 @@ packages: timestamp: 1728377334097 - pypi: ./ name: upstage-des - version: 0.4.0 - sha256: bdb26261f86fd8911ce19ff7e99142cb776d4e4a3de0f87bf694a51a3b54087e + version: 1.0.0 + sha256: 966dd9a5d916b6460463e2944d9cdc2af39ea6e67b34eb7e4bdb13aa0aaac16c requires_dist: - simpy>=4 - myst-parser ; extra == 'docs' @@ -8199,7 +7076,7 @@ packages: - pytest-html ; extra == 'test' - pytest-json-report ; extra == 'test' - pytest-xdist ; extra == 'test' - requires_python: '>=3.11' + requires_python: '>=3.12' editable: true - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 @@ -8425,21 +7302,6 @@ packages: - pkg:pypi/zipp?source=hash-mapping size: 22963 timestamp: 1749421737203 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py311h9ecbd09_1.conda - sha256: 1a824220227f356f35acec5ff6a4418b1ccd0238fd752ceebeb04a0bd37acf0f - md5: 6d229edd907b6bb39961b74e3d52de9c - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.11 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=compressed-mapping - size: 732182 - timestamp: 1741853419018 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_1.conda sha256: b4fd6bd1cb87a183a8bbe85b4e87a1e7c51473309d0d82cd88d38fb021bcf41e md5: d28b82fcc8d1b462b595af4b15a6cdcf @@ -8470,20 +7332,6 @@ packages: - pkg:pypi/zstandard?source=hash-mapping size: 737893 timestamp: 1741853442447 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py311h4d7f069_1.conda - sha256: 7810fa3c45a93679eb78b49f1a4db0397e644dbb0edc7ff6e956668343f4f67f - md5: 11d2b64d86f2e63f7233335a23936151 - depends: - - __osx >=10.13 - - cffi >=1.11 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 690324 - timestamp: 1741853501630 - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py312h01d7ebd_1.conda sha256: 5d2635e81ff5d61c87383c62824988154acefeae63f408d03dbefcb80cba5f02 md5: 493516415601e57f73bda23e91dda742 @@ -8512,21 +7360,6 @@ packages: - pkg:pypi/zstandard?source=compressed-mapping size: 692765 timestamp: 1741853628130 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311h917b07b_1.conda - sha256: 496189ea504358088128df526e545a96d7c8b597bea0747f09bc0e081a67a69b - md5: be18ca5f35d991ab12342a6fc3f7a6f8 - depends: - - __osx >=11.0 - - cffi >=1.11 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 532580 - timestamp: 1741853536042 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312hea69d52_1.conda sha256: db7ed45ce0ed42de5b799c094f15c064e5e7e88bbee128f8d15a0565367f3c41 md5: b0af1b749dbf9621fbea742c2de68ff8 @@ -8557,22 +7390,6 @@ packages: - pkg:pypi/zstandard?source=hash-mapping size: 536091 timestamp: 1741853541598 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py311he736701_1.conda - sha256: 78afa8ce76763993a76da1b0120b690cba8926271cc9e0462f66155866817c84 - md5: a4c147aaaf7e284762d7a6acc49e35e5 - depends: - - cffi >=1.11 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 444456 - timestamp: 1741853849446 - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py312h4389bb4_1.conda sha256: 17f2abbda821be146b549498fab3d0eb9cafb210e163b983524db91524b8dcb5 md5: 5028543ffb67666ca4fc3ebd620c97b8 diff --git a/pixi.toml b/pixi.toml index 4b30b3a..672fda6 100644 --- a/pixi.toml +++ b/pixi.toml @@ -7,7 +7,7 @@ platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"] upstage_des = { path=".", editable=true} [dependencies] -python = ">=3.11" +python = ">=3.12" # generic tasks ########################################### [tasks] @@ -61,7 +61,7 @@ inputs = ["src/**/*.py"] [feature.tasks-docs.tasks.autogen] description = "Autobuild the code documentation" -cmd = "sphinx-apidoc -f -o ./docs/source/auto ./src/upstage_des ./src/upstage_des/test" +cmd = "sphinx-apidoc -f -o ./docs/source/auto ./src/upstage_des" [feature.tasks-docs.tasks.build_html_docs] description = "Build the documentation" @@ -89,15 +89,6 @@ features = [ "tasks-jlite", ] -[environments.py311] -features = [ - "py311", - "deps-lint", - "deps-test", - "deps-docs", - "tasks-lint", - "tasks-test", -] [environments.py312] features = [ @@ -122,9 +113,6 @@ features = [ "tasks-test", ] -[feature.py311.dependencies] -python = "3.11.*" - [feature.py312.dependencies] python = "3.12.*" diff --git a/pyproject.toml b/pyproject.toml index 4ab79b9..50cc851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires = [ [project] name = "upstage-des" -version = "0.4.0" +version = "1.0.0" description = "A library for behavior-driven discrete event simulation." readme = "README.md" keywords = [ @@ -23,7 +23,7 @@ license = { file = "LICENSE" } authors = [ { name = "James Arruda", email = "James.Arruda@gtri.gatech.edu" }, ] -requires-python = ">=3.11" +requires-python = ">=3.12" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -33,7 +33,6 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", @@ -62,17 +61,16 @@ optional-dependencies.test = [ ] urls."Bug Tracker" = "https://github.com/gtri/upstage/issues" urls.Documentation = "https://gtri.github.io/upstage" -urls."Source" = "https://github.com/gtri/upstage" +urls.Source = "https://github.com/gtri/upstage" -[tool.flit.sdist] -exclude = [ +[tool.flit] +sdist.exclude = [ ".gitignore", ] [tool.ruff] line-length = 100 cache-dir = "build/.ruff_cache" - format.docstring-code-line-length = 100 lint.select = [ "D", @@ -86,20 +84,19 @@ lint.select = [ lint.ignore = [ "D105", ] -lint.per-file-ignores."src/upstage_des/test/__*.py" = [ +lint.per-file-ignores."tests/__*.py" = [ "D", ] -lint.per-file-ignores."src/upstage_des/test/test*.py" = [ +lint.per-file-ignores."tests/test*.py" = [ "D", ] lint.pydocstyle.convention = "google" -[tool.pytest.ini_options] -junit_family = "xunit2" -cache_dir = "build/.pytest_cache" -addopts = [ - "--pyargs", - "upstage_des", +[tool.pytest] +ini_options.junit_family = "xunit2" +ini_options.cache_dir = "build/.pytest_cache" +ini_options.testpaths = [ "tests/" ] +ini_options.addopts = [ # for contributors "--cov-report=term-missing:skip-covered", "--color=yes", @@ -120,42 +117,35 @@ addopts = [ "--ff", ] -[tool.coverage.run] -data_file = "build/reports/.coverage" -omit = [ - "*/test/test*.py", - "*/test/test_nucleus_state_share/*.py", +[tool.coverage] +run.data_file = "build/reports/.coverage" +run.omit = [ "*/upstage_des/utils.py", + "tests/test*.py", ] - -[tool.coverage.html] -show_contexts = true - -[tool.coverage.paths] -upstage_des = [ +paths.upstage_des = [ "src/upstage_des", "*/src/upstage_des", ] - -[tool.coverage.report] -exclude_also = [ - "raise UpstageError*", - "raise SimulationError*", - "raise NotImplementedError*", +report.exclude_also = [ "except LookupError", + "except MotionAndDetectionError", "except RuntimeError", "except SimulationError", - "raise TypeError", "except TypeError", "raise MotionAndDetectionError", - "except MotionAndDetectionError", + "raise NotImplementedError*", + "raise SimulationError*", + "raise TypeError", + "raise UpstageError*", ] +html.show_contexts = true [tool.mypy] # broken by ipywidgetsplugins = "pydantic.mypy" cache_dir = "build/.mypy_cache" sqlite_cache = true -python_version = "3.11" +python_version = "3.12" allow_redefinition = true check_untyped_defs = true disallow_untyped_defs = true @@ -164,13 +154,8 @@ show_error_codes = true warn_return_any = true warn_unused_ignores = true disable_error_code = "type-abstract" -# disallow_any_unimported = true -#exclude = [ -# "^.+/upstage_des/test.+\\.py$", -#] - -[[tool.mypy.overrides]] -module = [ - "importlib.metadata", +overrides = [ + { module = [ + "importlib.metadata", + ], ignore_missing_imports = true }, ] -ignore_missing_imports = true diff --git a/src/upstage_des/__init__.py b/src/upstage_des/__init__.py new file mode 100644 index 0000000..898b6d1 --- /dev/null +++ b/src/upstage_des/__init__.py @@ -0,0 +1,70 @@ +"""upstage_des: Discrete event simulation library built on SimPy.""" + +from upstage_des.actor import EMPTY_KNOWLEDGE, Actor +from upstage_des.base import ( + ENTITY_REGISTRY_CONTEXT_VAR, + ENV_CONTEXT_VAR, + STAGE_CONTEXT_VAR, + EnvironmentContext, + SimulationError, + Stage, + UpstageBase, + UpstageError, + add_stage_variable, + clear_top_context, + create_top_context, + get_entities_by_class, + get_entity_registry, + get_stage, + get_stage_variable, +) +from upstage_des.events import ( + Any, + Event, + FilterGet, + Get, + Put, + ResourceHold, + Wait, +) +from upstage_des.states import ( + LinearChangingState, + State, +) +from upstage_des.tasks import ( + DecisionTask, + InterruptStates, + Task, +) + +__all__ = [ + "Actor", + "EMPTY_KNOWLEDGE", + "ENTITY_REGISTRY_CONTEXT_VAR", + "ENV_CONTEXT_VAR", + "STAGE_CONTEXT_VAR", + "EnvironmentContext", + "SimulationError", + "Stage", + "UpstageBase", + "UpstageError", + "add_stage_variable", + "clear_top_context", + "create_top_context", + "get_entities_by_class", + "get_entity_registry", + "get_stage", + "get_stage_variable", + "Any", + "Event", + "FilterGet", + "Get", + "Put", + "ResourceHold", + "Wait", + "LinearChangingState", + "State", + "DecisionTask", + "InterruptStates", + "Task", +] diff --git a/src/upstage_des/actor.py b/src/upstage_des/actor.py new file mode 100644 index 0000000..e19ae9b --- /dev/null +++ b/src/upstage_des/actor.py @@ -0,0 +1,544 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Actor system with dataclass-like field transformation and State descriptors.""" + +from collections import OrderedDict, defaultdict, deque +from collections.abc import Iterable +from copy import deepcopy +from dataclasses import dataclass +from typing import Any, Self, dataclass_transform + +from simpy import Process + +from upstage_des.base import ( + SimulationError, + UpstageBase, + UpstageError, +) +from upstage_des.root_types import StateDataDict +from upstage_des.states import LinearChangingState, State, _ActiveState + +EMPTY_KNOWLEDGE = object() + + +@dataclass +class TaskData: + """Data about a task process on an Actor.""" + + name: str + process: Process + + +def _process_model_class(cls: type[Any]) -> None: + """Apply dataclass-like behavior + ModelField descriptors to the class.""" + model_fields: OrderedDict[str, State] = OrderedDict() + my_fields: list[str] = [] + for base in reversed(cls.__mro__): + if base is object: + continue + annotations = getattr(base, "__annotations__", {}) + for name in annotations: + if name.startswith("_") or name in {"__weakref__", "__dict__", "__model_fields__"}: + continue + + # Use State if defined, else create one + if name in base.__dict__ and isinstance(base.__dict__[name], State): + field_obj = base.__dict__[name] + else: + default = base.__dict__.get(name, ...) + if default is ...: + field_obj = State() + if name == "knowledge": + field_obj._default_factory = dict + else: + field_obj = State(default=default) + # In all cases, drop the type info into the data + field_obj._add_type(annotations[name]) + + if not getattr(field_obj, "name", None): + field_obj.__set_name__(cls, name) + + model_fields[name] = field_obj + if base is cls: + my_fields.append(name) + # set attrs _after_, which helps inheritence + model_fields_dict: dict[str, State[Any]] = {} + for name, field_obj in model_fields.items(): + model_fields_dict[name] = field_obj + setattr(cls, name, field_obj) + cls.__model_fields__ = model_fields_dict + + def __init__(self: Any, **kwargs: Any) -> None: + # Set up the data storage + self._state_histories = {} + self._log = deque() + self._is_clone = False + self._state_data = {} + self._states_by_cause = defaultdict(set) + self._causes_by_state = {} + + for name in model_fields.keys(): + value = kwargs.pop(name, ...) + field = self.__model_fields__[name] + self._state_data[name] = {} + if value is ...: + if not field.has_default: + raise ValueError(f"No input supplied for state: {name}") + field._set_default(self) + else: + field.__set__(self, value) + UpstageBase.__init__(self, **kwargs) + if hasattr(cls, "__post_init__"): + cls.__post_init__(self) + + cls.__init__ = __init__ + + +@dataclass_transform( + kw_only_default=True, + frozen_default=False, + field_specifiers=(State,), +) +class _BaseActor(UpstageBase): + """Base class for actors with automatic field processing and State support. + + Actors automatically process type annotations to create descriptors: + - State[T] annotations become State descriptors with hooks and validation + - Other annotations become ModelField descriptors with default value support + + Attributes: + name: The actor's name + debug_logging: Whether to enable debug logging + is_clone: Whether this actor is a clone (set by clone() method) + + Example: + >>> class MyActor(BaseAct): + ... name: str + ... fuel: State[float] = State(default=100.0) + ... + >>> actor = MyActor(name="vehicle") + >>> actor.fuel + 100.0 + """ + + name: str + knowledge: dict[str, Any] + + _is_clone: bool + _state_histories: dict[str, deque[tuple[float, Any] | tuple[float, Any, Any]]] + _log: deque[tuple[float, str]] + _state_data: dict[str, StateDataDict] + __model_fields__: dict[str, State] + _states_by_cause: dict[Any, set[str]] + _causes_by_state: dict[str, Any] + + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + # Apply the model transformation to every subclass + _process_model_class(cls) + + @property + def is_clone(self) -> bool: + """Return whether this actor is a clone.""" + return getattr(self, "_is_clone", False) + + def _record_state_change(self, name: str, value: Any, extra: Any | None = None) -> None: + if name in ["name", "debug_logging", "is_clone", "knowledge"]: + return + time = self.env.now + if name not in self._state_histories: + self._state_histories[name] = deque() + to_append = (time, value) if extra is None else (time, value, extra) + self._state_histories[name].append(to_append) + + ########################################################### + ### Logging ############################################## + def write_to_log(self, to_write: str) -> None: + """Write to the log. + + Args: + to_write (str): The text to write + """ + self._log.append((self.env.now, to_write)) + + def get_log(self) -> deque[tuple[float, str]]: + """Retrieve the log. + + Returns: + deque[tuple[float, str]]: The log. + """ + return self._log + + ########################################################### + ### Knowledge Helpers ##################################### + def get_knowledge(self, name: str, must_exist: bool = False) -> Any: + """Get a piece of knowledge by name. + + If the knowledge doesn't exist, and it must not exist, this return + upstage_des.actor.EMPTY_KNOWLEDGE. Test equivalence using `is` to ensure + you aren't getting a None you meant to set. + + Args: + name (str): Name + must_exist (bool, optional): If the knowledge must exist. + Defaults to False. + + Raises: + SimulationError: If the knowledge must exist, but doesn't + + Returns: + Any: The value of the knowledge entry. + """ + if name not in self.knowledge: + if must_exist: + raise SimulationError(f"Knowledge {name} does not exist on {self.name}") + return EMPTY_KNOWLEDGE + return self.knowledge[name] + + def set_knowledge( + self, name: str, value: Any, overwrite: bool = True, caller: Any = "" + ) -> None: + """Set knowledge, checking for overwrite and logging who set it. + + Args: + name (str): Knowledge name + value (Any): Knowledge value + caller (Any): This gets logged as the reason for setting knowledge. + overwrite (bool, optional): Allow existing knowledge to be overwritten. + Defaults to True. + + Raises: + SimulationError: If existing knowledge would be overwritten without permission. + """ + self.write_to_log(f"Setting {name} knowledge. Reason: {caller}") + if self.get_knowledge(name) is EMPTY_KNOWLEDGE or overwrite: + self.knowledge[name] = value + return + raise SimulationError(f"Knowledge {name} is already set, and overwrite is {overwrite}") + + def clear_knowledge(self, name: str, caller: Any = "") -> None: + """Delete knowledge from the dictonary. + + Args: + name (str): The name + caller (str): Who is calling this method. Defaults to "". + """ + self.write_to_log(f"Clearing {name} knowledge. Reason: {caller}") + if name in self.knowledge: + del self.knowledge[name] + + def get_and_clear_knowledge(self, name: str, caller: Any = "") -> Any: + """Get a knowledge value and clear it. + + The knowledge is assumed to exist. + + Args: + name (str): The name + caller (str): Who is calling this method. Defaults to "". + + Raises: + SimulationError: If the knowledge doesn't exist + + Returns: + Any: Knowledge value + """ + know = self.get_knowledge(name, must_exist=True) + self.clear_knowledge(name, caller=caller) + return know + + ########################################################### + ### Activate States ####################################### + def _lock_state(self, *, state: str, cause: Any) -> None: + """Lock one of the actor's states by a given cause. + + Args: + state (str): The name of the state to lock + cause (Task): The cause that is locking the state + """ + # single-task only, so no task should + # be associated with this state + if state in self._causes_by_state: + raise SimulationError( + f"State '{state}' cannot be used by '{cause}' because it is " + f"locked by {self._causes_by_state[state]}" + ) + self._states_by_cause[cause].add(state) + self._causes_by_state[state] = cause + + def _unlock_state(self, *, state: str, cause: Any) -> None: + """Unlock one of the actor's states by a given cause. + + If the cause didn't lock the state, an error is raised. + + Args: + state (str): The name of the state to lock + cause (Task): The cause that is unlocking the state + """ + if self._causes_by_state.get(state, None) is not cause: + raise SimulationError(f"State '{state}' cannot was not used by '{cause}'") + self._states_by_cause[cause].remove(state) + del self._causes_by_state[state] + + def activate_state(self, state: str, *, cause: Any, **state_kwargs: Any) -> None: + """Activate a state. + + Args: + state (str): The name of the state to activate + cause (Any): Unique identifier or object for who activated the state. + state_kwargs (Any): Arguments to pass to the state activation. + """ + _state = self.__model_fields__[state] + assert isinstance(_state, _ActiveState) + self._lock_state(state=state, cause=cause) + _state.activate(self, **state_kwargs) + + def deactivate_state(self, state: str, *, cause: Any, **kwargs: Any) -> None: + """Deactivate an already active state. + + Args: + state (str): Name of the state + cause (Any): Unique identifier or object for who activated the state. + kwargs (Any): Arguments expected by the specific state. + """ + _state = self.__model_fields__[state] + assert isinstance(_state, _ActiveState) + self._unlock_state(state=state, cause=cause) + _state.deactivate(self, **kwargs) + + def deactivate_states(self, states: Iterable[str], *, cause: Any, **kwargs: Any) -> None: + """Deactivate already active states. + + Args: + states (Iterable[str]): Names of the states + cause (Any): Unique identifier or object for who activated the state. + kwargs (Any): Arguments expected by the specific state. + """ + for name in states: + self.deactivate_state(name, cause=cause, **kwargs) + + def deactivate_all_states(self, *, cause: Any, **kwargs: Any) -> None: + """Deactivate all running states. + + This allows no states to be deactivated if none were activated by the cause. + + Args: + cause (Any): Unique identifier or object for who activated the state. + kwargs (Any): Arguments expected by the states to deactivate. + """ + if cause not in self._states_by_cause: + return + state_names = list(self._states_by_cause[cause]) + self.deactivate_states(state_names, cause=cause, **kwargs) + + def activate_linear_state(self, state: str, rate: float, *, cause: Any) -> None: + """Activate a linear changing state. + + Args: + state (str): The state name + rate (float): The rate to change the state + cause (Any): Unique identifier or object for who is activating the state. + """ + _state = self.__model_fields__[state] + assert type(_state) is LinearChangingState + self.activate_state(state, rate=rate, cause=cause) + + def deactivate_linear_state(self, state: str, *, cause: Any) -> None: + """Deactivate a linear changing state. + + Exists as a pair to `activate_linear_state`. + + Args: + state (str): The state name + cause (Any): Unique identifier or object for who activated the state. + """ + _state = self.__model_fields__[state] + assert type(_state) is LinearChangingState + self.deactivate_state(state, cause=cause) + + def make_event_for_state_goal(self, state: str, goal_value: float) -> float | None: + """Get the time when a linear changing state will reach a goal value. + + Args: + state (str): The state name + goal_value (float): The target value + + Returns: + float | None: The absolute time when the state will reach the goal value, + or None if the state is not active or the goal is unreachable. + """ + _state = self.__model_fields__[state] + predict_method = getattr(_state, "predict_value_time", None) + if predict_method is None: + return None + try: + result = predict_method(self, goal_value) + return result # type: ignore[no-any-return] + except Exception: + return None + + ########################################################### + ### Cloning ############################################### + def clone(self) -> Self: + """Create a deep copy of this actor with current state values. + + The clone: + - Has all state values deep-copied from the current actor + - Does not copy any state values that are actors + - Has no state history + - Is marked with is_clone=True + - Is not registered in the entity registry + + Returns: + Self: A cloned actor with the same state values + """ + kwargs = {} + for field_name, field_obj in self.__model_fields__.items(): + current_value = getattr(self, field_name) + if isinstance(current_value, Actor): + kwargs[field_name] = current_value + else: + kwargs[field_name] = deepcopy(current_value) + + cloned = type(self)(**kwargs) + cloned._state_histories = {} + cloned._is_clone = True + + return cloned + + def _clean(self) -> None: + """Run to clean all memory from the actor.""" + self._state_histories = {} + self._log = deque() + self._state_data = {} + self._states_by_cause = {} + self._causes_by_state = {} + + +class Actor(_BaseActor): + """The actor.""" + + name: str + debug_logging: bool = True + knowledge: dict[str, Any] + + +class ActorHelper: + """A mixin class for modifying an actor with logging help.""" + + def set_actor_knowledge( + self, + actor: Actor, + name: str, + value: Any, + overwrite: bool = False, + ) -> None: + """Set knowledge on the actor. + + Convenience method for passing in the name of task for actor logging. + + Args: + actor (Actor): The actor to set knowledge on. + name (str): Name of the knowledge + value (Any): Value of the knowledge + overwrite (bool, optional): Allow overwrite or not. Defaults to False. + """ + cname = self.__class__.__qualname__ + actor.set_knowledge(name, value, overwrite=overwrite, caller=cname) + + def clear_actor_knowledge(self, actor: Actor, name: str) -> None: + """Clear knowledge from an actor. + + Convenience method for passing in the name of task for actor logging. + + Args: + actor (Actor): The actor to clear knowledge from + name (str): The name of the knowledge + """ + cname = self.__class__.__qualname__ + actor.clear_knowledge(name, caller=cname) + + @staticmethod + def get_actor_knowledge(actor: Actor, name: str, must_exist: bool = False) -> Any: + """Get knowledge from the actor. + + Args: + actor (Actor): The actor to get knowledge from. + name (str): Name of the knowledge + must_exist (bool, optional): Raise errors if the knowledge doesn't exist. + Defaults to False. + + Returns: + Any: The knowledge value, which could be None + """ + return actor.get_knowledge(name, must_exist) + + def get_and_clear_actor_knowledge(self, actor: Actor, name: str) -> Any: + """Get and clear knowledge on an actor. + + The knowledge is assumed to exist. + + Args: + actor (Actor): The actor to get knowledge from. + name (str): The knowledge name. + + Returns: + Any: The knowledge value. + """ + cname = self.__class__.__qualname__ + return actor.get_and_clear_knowledge(name, caller=cname) + + def set_actor_bulk_knowledge( + self, actor: Actor, know: dict[str, Any], overwrite: bool = False + ) -> None: + """Set multiple knowledge entries at once. + + Args: + actor (Actor): The actor to operate on. + know (dict[str, Any]): Dictionary of key:value pairs of knowledge. + overwrite (bool, optional): If overwrite is allowed. Defaults to False. + """ + for k, v in know.items(): + self.set_actor_knowledge(actor, k, v, overwrite) + + def clear_actor_bulk_knowledge(self, actor: Actor, names: Iterable[str]) -> None: + """Clear a list of knowledge entries. + + Args: + actor (Actor): The actor to operate on. + names (Iterable[str]): Knowledge names. + """ + for name in names: + self.clear_actor_knowledge(actor, name) + + def get_actor_bulk_knowledge( + self, actor: Actor, names: Iterable[str], must_exist: bool = False + ) -> dict[str, Any]: + """Get multiple knowledge items. + + Args: + actor (Actor): The actor to operate on. + names (Iterable[str]): Names of the knowledge + must_exist (bool, optional): If all entires must exist. Defaults to False. + + Returns: + dict[str, Any]: The knowledge values. None if not present. + """ + return {name: self.get_actor_knowledge(actor, name, must_exist) for name in names} + + def get_and_clear_actor_bulk_knowledge( + self, actor: Actor, names: Iterable[str], caller: str | None = None + ) -> dict[str, Any]: + """Get and clear multiple knowledge entries. + + Args: + actor (Actor): The actor to operate on. + names (Iterable[str]): The knowledge to retrieve and delete. + caller (str | None, optional): The name of the caller. Defaults to None. + + Returns: + dict[str, Any]: The retrieved knowledge. + """ + return {name: self.get_and_clear_actor_knowledge(actor, name) for name in names} diff --git a/src/upstage_des/base.py b/src/upstage_des/base.py new file mode 100644 index 0000000..e3f2b8d --- /dev/null +++ b/src/upstage_des/base.py @@ -0,0 +1,468 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Base classes and exceptions for upstage_des.""" + +from collections.abc import Callable, Generator +from contextvars import ContextVar, Token +from dataclasses import dataclass, field +from functools import wraps +from random import Random +from typing import Any +from warnings import warn + +from simpy import Environment as SimpyEnv +from simpy import Event as SimpyEvent +from simpy import Process + +CONTEXT_ERROR_MSG = "Undefined context variable: use EnvironmentContext" + + +SIMPY_GEN = Generator[SimpyEvent, Any, Any] + + +class UpstageError(Exception): + """Raised when an UPSTAGE error happens or expectation is not met.""" + + +@dataclass +class Stage: + """Simulation stage configuration and shared state. + + The Stage holds global simulation configuration (units, time settings) and shared + state (RNG, custom data) accessible to all simulation components via context. + + Warnings are raised if you re-set a value. + + Attributes: + random: Random number generator for simulation stochasticity. + altitude_units: Units for altitude measurements (e.g., "ft", "m"). + distance_units: Units for distance measurements (e.g., "nmi", "km"). + time_unit: Base time unit for simulation (e.g., "hr", "min", "s"). + Affects `pretty_now` formatting and can be used in `Wait` timeouts. + daily_time_count: Number of time_units in a "day" for time formatting. + Only used when time_unit is not "s", "min", or "hr" (which assume 24-hour days). + debug_log_time: Whether to log times as formatted strings in debug logs. + Can be overridden at the individual actor level. + userdata: A blank dictionary for runtime data + """ + + random: Random = field(default_factory=Random) + altitude_units: str = "ft" + distance_units: str = "nmi" + time_unit: str = "hr" + daily_time_count: float = 24.0 + debug_log_time: bool = False + userdata: dict = field(default_factory=dict) + managers: dict = field(default_factory=dict) + + _set: set = field(default_factory=set, init=False, repr=False) + + def __setattr__(self, name: str, value: object) -> None: + if name.startswith("_"): + super().__setattr__(name, value) + return + if name == "userdata" or name == "managers": + super().__setattr__(name, value) + return + if hasattr(self, "_set") and name in self._set: + raise UpstageError(f"Stage attribute '{name}' can only be set once") + super().__setattr__(name, value) + if hasattr(self, "_set"): + self._set.add(name) + + +class SimulationError(UpstageError): + """Raised when a simulation error occurs.""" + + def __init__(self, message: str, time: float | None = None): + """Create an informative simulation error. + + Args: + message (str): Error message + time (float | None, optional): Time of the error. Defaults to None. + """ + msg = "Error in the simulation: " + if msg in message: + msg = "" + msg += f" at time {time}: " if time is not None else "" + self.message = msg + message + super().__init__(self.message) + + +ENV_CONTEXT_VAR: ContextVar[SimpyEnv] = ContextVar("Environment") +STAGE_CONTEXT_VAR: ContextVar[Stage] = ContextVar("Stage") +ENTITY_REGISTRY_CONTEXT_VAR: ContextVar[dict[str, list[Any]]] = ContextVar("EntityRegistry") +REHEARSAL_CONTEXT_VAR: ContextVar[bool] = ContextVar("Rehearsing") + + +class UpstageBase: + """A base mixin class for everyone. + + Provides access to all context variables created by `EnvironmentContext`. + + >>> with EnvironmentContext(initial_time=0.0) as env: + >>> data = UpstageBase() + >>> assert data.env is env + """ + + def _register_entity(self) -> None: + try: + registry = ENTITY_REGISTRY_CONTEXT_VAR.get() + except LookupError: + return + + for cls in type(self).__mro__: + if cls is object: + continue + class_name = cls.__name__ + if class_name not in registry: + registry[class_name] = [] + if self not in registry[class_name]: + registry[class_name].append(self) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Simple init to check if environment should be set.""" + try: + _ = self.env + except UpstageError: + warn(f"Environment not created at instantiation of {self}") + self._register_entity() + super().__init__() + + @property + def env(self) -> SimpyEnv: + """Return the environment. + + Returns: + SimpyEnv: SimPy environment. + """ + try: + env: SimpyEnv = ENV_CONTEXT_VAR.get() + except LookupError: + raise UpstageError("No environment found or set.") + return env + + @property + def stage(self) -> Stage: + """Return the stage context variable. + + Returns: + StageProtocol: The stage, as defined in context. + + Raises: + LookupError: If no stage context is available + """ + return STAGE_CONTEXT_VAR.get() + + @property + def entity_registry(self) -> dict[str, list[Any]]: + """Return the entity registry. + + Returns: + dict[str, list[Any]]: Dictionary mapping class names to lists of instances + """ + try: + registry = ENTITY_REGISTRY_CONTEXT_VAR.get() + except LookupError: + raise UpstageError("No entity registry found or set.") + return registry + + +class EnvironmentContext: + """A context manager to create a safe, globally (in context) referenceable environment and data. + + The environment created is of type simpy.Environment + + This also sets context variables for actors, entities, and the stage. + + Usage: + >>> with EnvironmentContext(initial_time=0.0) as env: + >>> env.run(until=3.0) + + This context manager is meant to be paired with inheritors of `UpstageBase`. + + that provides access to the context variables created in this manager. + + >>> class SimData(UpstageBase): + >>> ... + >>> + >>> with EnvironmentContext(initial_time=0.0) as env: + >>> data = SimData() + >>> assert data.env is env + + You may also provide a random seed, and a default Random() will be created with + that seed. + + >>> with EnvironmentContext(random_seed=1234986) as env: + >>> UpstageBase().stage.random.uniform(1, 3) + ... 2.348057489610457 + + Or your own RNG: + + >>> rng = Random(1234986) + >>> with EnvironmentContext(random_gen=rng) as env: + >>> UpstageBase().stage.random.uniform(1, 3) + ... 2.348057489610457 + """ + + def __init__( + self, + initial_time: float = 0.0, + random_seed: int | None = None, + random_gen: Any | None = None, + stage: Stage | None = None, + ) -> None: + """Create an environment context. + + random_seed is ignored if random_gen is given. Otherwise random.Random is + used. If stage is provided, it will be used instead of creating a new Stage. + + Args: + initial_time (float, optional): Time to start the clock at. Defaults to 0.0. + random_seed (int | None, optional): Seed for RNG. Defaults to None. + random_gen (Any | None, optional): RNG object. Defaults to None. + stage (Stage | None, optional): Stage instance to use. Defaults to None. + """ + self.env_ctx = ENV_CONTEXT_VAR + self.stage_ctx = STAGE_CONTEXT_VAR + self.entity_registry_ctx = ENTITY_REGISTRY_CONTEXT_VAR + self.rehearsal_ctx = REHEARSAL_CONTEXT_VAR + self.env_token: Token[SimpyEnv] + self.stage_token: Token[Stage] + self.entity_registry_token: Token[dict[str, list[Any]]] + self.rehearsal_token: Token[bool] + self._env: SimpyEnv | None = None + self._initial_time: float = initial_time + self._random_seed: int | None = random_seed + self._random_gen: Any = random_gen + self._stage: Stage | None = stage + + def __enter__(self) -> SimpyEnv: + """Create the environment context. + + Returns: + SimpyEnv: Simpy Environment + """ + self._env = SimpyEnv(initial_time=self._initial_time) + self.env_token = self.env_ctx.set(self._env) + + if self._stage is not None: + stage = self._stage + if stage.random is not None: + if self._random_seed is not None: + stage.random.seed(self._random_seed) + elif self._random_gen is not None: + stage.random = self._random_gen + else: + if self._random_gen is None: + random_gen = Random(self._random_seed) + else: + random_gen = self._random_gen + + stage = Stage(random=random_gen) + + self.stage_token = self.stage_ctx.set(stage) + + entity_registry: dict[str, list[Any]] = {} + self.entity_registry_token = self.entity_registry_ctx.set(entity_registry) + + self.rehearsal_token = self.rehearsal_ctx.set(False) + + return self._env + + def __exit__(self, *_: Any) -> None: + """Leave the context.""" + self.env_ctx.reset(self.env_token) + self.stage_ctx.reset(self.stage_token) + self.entity_registry_ctx.reset(self.entity_registry_token) + self.rehearsal_ctx.reset(self.rehearsal_token) + self._env = None + + +def add_stage_variable(varname: str, value: Any) -> None: + """Add a variable to the stage. + + Uses `userdata` if the variable doesn't exist. + + Args: + varname (str): Name of the variable + value (Any): Value to set it as + """ + try: + stage = STAGE_CONTEXT_VAR.get() + except LookupError: + raise ValueError("Stage should have been set.") + if varname in stage.__dataclass_fields__: + raise UpstageError(f"Variable '{varname}' already exists in the stage") + # otherwise go to userdata + if varname in stage.userdata: + raise UpstageError(f"Variable '{varname}' already exists in the stage userdata") + stage.userdata[varname] = value + + +def get_stage_variable(varname: str) -> Any: + """Get a variable from the context's stage. + + Args: + varname (str): Name of the variable + + Returns: + Any: The variable's value + """ + try: + stage = STAGE_CONTEXT_VAR.get() + except LookupError: + raise ValueError("Stage should have been set.") + if varname not in stage.__dataclass_fields__: + if varname in stage.userdata: + return stage.userdata[varname] + raise UpstageError(f"Variable '{varname}' does not exist in the stage or userdata") + return getattr(stage, varname) + + +def get_stage() -> Stage: + """Return the entire stage object. + + Returns: + StageProtocol: The stage + + Raises: + LookupError: If no stage context is available + """ + return STAGE_CONTEXT_VAR.get() + + +def create_top_context( + initial_time: float = 0.0, + random_seed: int | None = None, + random_gen: Any | None = None, + stage: Stage | None = None, +) -> EnvironmentContext: + """Create a stage at this level of context. + + Makes your current level the same as the context manager. + + Args: + initial_time (float, optional): Time to start the clock at. Defaults to 0.0. + random_seed (int | None, optional): Seed for RNG. Defaults to None. + random_gen (Any | None, optional): RNG object. Defaults to None. + stage (Stage | None, optional): Stage instance to use. Defaults to None. + + Returns: + EnvironmentContext: The context + """ + ctx = EnvironmentContext(initial_time, random_seed, random_gen, stage) + ctx.__enter__() + return ctx + + +def clear_top_context(ctx: EnvironmentContext) -> None: + """Clear the context. + + Args: + ctx (EnvironmentContext): The object made from create_stage() + """ + ctx.__exit__() + + +def get_entity_registry() -> dict[str, list[Any]]: + """Return the entity registry. + + Returns: + dict[str, list[Any]]: Dictionary mapping class names to lists of instances + """ + try: + registry = ENTITY_REGISTRY_CONTEXT_VAR.get() + except LookupError: + raise ValueError("Entity registry should have been set.") + return registry + + +def get_entities_by_class(class_name: str) -> list[Any]: + """Get all entities of a specific class name. + + Args: + class_name (str): The name of the class + + Returns: + list[Any]: List of entity instances + """ + registry = get_entity_registry() + return registry.get(class_name, []) + + +def set_rehearsing(value: bool) -> None: + """Set the rehearsal context var. + + When True, it prevents @process from going to simpy. + + Args: + value (bool): The rehearsal value to set. + """ + REHEARSAL_CONTEXT_VAR.set(value) + + +PROC = Generator[SimpyEvent, Any, None] + + +def process( + func: Callable[..., PROC], +) -> Callable[..., Process | PROC]: + """Decorate a ``simpy`` process to schedule it as a callable. + + Allows users to decorate a generator, and when they want to schedule them + as a ``simpy`` process, they can simply call it, e.g., instead of calling: + + Usage: + + >>> from upstage_des.api import process, Wait + ... + >>> @process + >>> def generator(wait_period=1.0, msg="Finished Waiting"): + >>> # A simple process that periodically prints a statement + >>> while True: + >>> yield Wait(wait_period).as_event() + >>> print(msg) + ... + >>> @process + >>> def another_process(): + >>> # Some other process that calls the first one + >>> generator() + + Args: + func (Callable[..., Generator[BaseEvent, None, None]]): The process function that is a + generator of simpy events. + + Returns: + Process: The generator as a ``simpy`` process. + + Note: + The value of this decorator is that it reduces the chance of a user + forgetting to call the generator as a process, which tends to produce + behaviors that are difficult to troubleshoot because the code will + build and can run, but the simulation will not work schedule the + process defined by the generator. + + """ + + @wraps(func) + def wrapped_generator(*args: Any, **kwargs: Any) -> Process | PROC: + """Wrap the generator with a function that calls it as a process.""" + try: + environment = ENV_CONTEXT_VAR.get() + except LookupError: + raise SimulationError("No environment found on process call") + try: + rehearsing = REHEARSAL_CONTEXT_VAR.get() + except LookupError: + rehearsing = False + f = func(*args, **kwargs) + if not rehearsing: + return environment.process(f) + else: + return f + + return wrapped_generator diff --git a/src/upstage_des/cleanup.py b/src/upstage_des/cleanup.py new file mode 100644 index 0000000..7cc7a0c --- /dev/null +++ b/src/upstage_des/cleanup.py @@ -0,0 +1,17 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Functions to help clean/clear the sim out.""" + +from upstage_des.actor import Actor + + +def clean_actor(actor: Actor) -> None: + """End all tasks and delete actor memory. + + Args: + actor (Actor): The actor to clean. + """ + actor._clean() diff --git a/src/upstage_des/data_utils/__init__.py b/src/upstage_des/data_utils/__init__.py new file mode 100644 index 0000000..b5e16b5 --- /dev/null +++ b/src/upstage_des/data_utils/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Data gathering utils.""" diff --git a/src/upstage_des/events.py b/src/upstage_des/events.py new file mode 100644 index 0000000..fb6bf95 --- /dev/null +++ b/src/upstage_des/events.py @@ -0,0 +1,624 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Classes for UPSTAGE events that feed to simpy.""" + +from collections.abc import Callable +from contextlib import suppress +from typing import Any as tyAny +from warnings import warn + +import simpy as SIM +from simpy.resources.container import ContainerGet, ContainerPut +from simpy.resources.resource import Release, Request +from simpy.resources.store import StoreGet, StorePut + +from upstage_des.base import SIMPY_GEN, SimulationError, UpstageBase, UpstageError +from upstage_des.units import unit_convert + +__all__ = ( + "All", + "Any", + "Event", + "MultiEvent", + "Wait", +) + +SIM_REQ_EVTS = ContainerGet | ContainerPut | StoreGet | StorePut | Request | Release + + +class BaseEvent(UpstageBase): + """Base class for framework events.""" + + def __init__(self, *, rehearsal_time_to_complete: float = 0.0): + """Create a base event with a notion of rehearsal time. + + Args: + rehearsal_time_to_complete (float, optional): Time to simulate passing + on rehearsal. Defaults to 0.0. + """ + super().__init__() + self._simpy_event: SIM.Event | None = None + + self.created_at: float = self.env.now + self.rehearsal_time_to_complete = rehearsal_time_to_complete + + def as_event(self) -> SIM.Event: + """Convert UPSTAGE event to a simpy Event. + + Returns: + SIM.Event: The upstage event as a simpy event. + """ + raise NotImplementedError( + "Events must specify how to convert to :class:`simpy.events.Event`" + ) + + def is_complete(self) -> bool: + """Is the event complete? + + Returns: + bool: If it's complete or not. + """ + if self._simpy_event is None: + raise UpstageError("Event has no simpy equivalent made.") + return self._simpy_event.processed + + def cancel(self) -> None: + """Cancel an event.""" + raise NotImplementedError("Implement custom event cancelling") + + +class MultiEvent(BaseEvent): + """A base class for evaluating multiple events. + + Note: + Subclasses of MultiEvent must define these methods: + * simpy_equivalent: simpy.Event + + For an example, refer to :class:`~Any` and :class:`~All`. + """ + + def __init__(self, *events: BaseEvent | SIM.Process) -> None: + """Create a multi-event based on a list of events. + + Args: + *events (BaseEvent): The events that comprise the multi-event. + """ + super().__init__() + + for event in events: + if not issubclass(event.__class__, BaseEvent): + warn( + f"Event '{event}' is not an upstage Event. " + f"All events in a MultiEvent must be an " + f"instance of upstage BaseEvent if you are going " + f"to rehearse the task that contains this MultiEvent.", + UserWarning, + ) + self.events = events + self._simpy_event = None + + @staticmethod + def simpy_equivalent(env: SIM.Environment, events: list[SIM.Event]) -> SIM.Event: + """Return the simpy equivalent event. + + Args: + env (SIM.Environment): The SimPy environment. + events (list[BaseEvent]): Events to turn into multi-event. + + Returns: + SIM.Event: The aggregate event. + """ + raise NotImplementedError("Implement in subclass") + + def _make_event(self, event: BaseEvent | SIM.Process) -> SIM.Event: + # handle a process in the MultiEvent for non-rehearsal uses + if isinstance(event, SIM.Process): + return event + return event.as_event() + + def as_event(self) -> SIM.Event: + """Convert the UPSTAGE event to simpy. + + Returns: + SIM.Event: typically an Any or All + """ + sub_events = [self._make_event(event) for event in self.events] + assert isinstance(self.env, SIM.Environment) + self._simpy_event = self.simpy_equivalent(self.env, sub_events) + return self._simpy_event + + def cancel(self) -> None: + """Cancel the multi event and propagate it to the sub-events.""" + if self._simpy_event is None: + raise UpstageError("Can't cancel a nonexistent event.") + self._simpy_event.defused = True + self._simpy_event.fail(Exception("defused")) + for event in self.events: + if isinstance(event, BaseEvent): + try: + event.cancel() + except Exception as e: + msg = f"Event {event} in {self} failed to cancel\n\t:{e}" + raise SimulationError(msg) + + +class Any(MultiEvent): + """An event that requires one event to succeed before succeeding.""" + + @staticmethod + def simpy_equivalent(env: SIM.Environment, events: list[SIM.Event]) -> SIM.Event: + """Return the SimPy version of the UPSTAGE Any event. + + Args: + env (SIM.Environment): SimPy Environment. + events (list[SIM.Event]): List of events. + + Returns: + SIM.Event: A simpy AnyOf event. + """ + return SIM.AnyOf(env, events) + + +class Event(BaseEvent): + """An UPSTAGE version of the standard SimPy Event. + + Returns a planning factor object on rehearsal for user testing against in rehearsals, in case. + + When the event is succeeded, a payload can be added through kwargs. + + This Event assumes that it might be long-lasting, and will auto-reset when yielded on. + """ + + def __init__( + self, + rehearsal_time_to_complete: float = 0.0, + auto_reset: bool = True, + ) -> None: + """Create an event. + + Args: + rehearsal_time_to_complete (float, optional): Expected time to complete. + Defaults to 0.0. + auto_reset (bool, optional): Whether to auto-reset on yield. Defaults to True. + """ + super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) + # The usage is sometimes that events might succeed before being + # yielded on + self._payload: dict[str, Any] = {} + self._auto_reset = auto_reset + assert isinstance(self.env, SIM.Environment) + self._simpy_event: SIM.Event = SIM.Event(self.env) + + def as_event(self) -> SIM.Event: + """Get the Event as a simpy type. + + This resets the event if allowed. + + Returns: + SIM.Event + """ + if self.is_complete(): + if self._auto_reset: + self.reset() + else: + raise UpstageError("Event not allowed to reset on yield.") + return self._simpy_event + + def succeed(self, **kwargs: tyAny) -> None: + """Succeed the event and store any payload. + + Args: + **kwargs (Any): key:values to store as payload. + """ + if self.is_complete(): + raise SimulationError("Event has already completed") + self._payload = kwargs + self._simpy_event.succeed() + + def get_payload(self) -> dict[str, tyAny]: + """Get any payload from the call to succeed(). + + Returns: + dict[str, Any]: The payload left by the succeed() caller. + """ + return self._payload + + def reset(self) -> None: + """Reset the event to allow it to be held again.""" + assert isinstance(self.env, SIM.Environment) + self._simpy_event = SIM.Event(self.env) + + def cancel(self) -> None: + """Cancel the event. + + Cancelling doesn't mean much, since it's still going to be yielded on. + """ + try: + self._simpy_event.defused = True + self._simpy_event.succeed() + except RuntimeError as exc: + exc.add_note(f"Runtime error when cancelling '{self}'") + raise exc + + +class Wait(BaseEvent): + """Wait a specified or random uniformly distributed amount of time. + + Return a timeout. If time is a list of length 2, choose a random time + between the interval given. + + Rehearsal time is given by the maximum time of the interval, if given. + + Parameters + ---------- + timeout : int, float, list, tuple + Amount of time to wait. If it is a list or a tuple of length 2, a + random uniform value between the two values will be used. + + """ + + def _convert_time(self, time: float | int, unit: str | None) -> float: + """Convert a time to the stage time. + + Args: + time (float | int): The current time + unit (str): Units the time is in + + Returns: + float: Time in stage units + """ + base_unit = self.stage.time_unit + if base_unit is not None and unit is not None: + return unit_convert(time, unit, base_unit) + return time + + def __init__( + self, + timeout: float | int, + timeout_unit: str | None = None, + *, + rehearsal_time_to_complete: float | int | None = None, + ) -> None: + """Create a timeout event. + + If timeout_unit is specified, UPSTAGE will try to convert it to the + time_unit set in the stage. Otherwise, it defaults to that time unit. + + Args: + timeout (float | int): Time to wait. + timeout_unit (str, optional): Units of time + rehearsal_time_to_complete (float | int, optional): The rehearsal time + to complete. Defaults to None (the timeout given). + + """ + if not isinstance(timeout, float | int): + raise SimulationError("Bad timeout. Did you mean to use from_random_uniform?") + timeout = self._convert_time(timeout, timeout_unit) + self._time_to_complete = timeout + self.timeout = timeout + if self._time_to_complete < 0: + raise SimulationError(f"Negative timeout in Wait: {self._time_to_complete}") + rehearse = timeout if rehearsal_time_to_complete is None else rehearsal_time_to_complete + super().__init__(rehearsal_time_to_complete=rehearse) + self._simpy_event: SIM.Timeout | None = None + + @classmethod + def from_random_uniform( + cls, + low: float | int, + high: float | int, + timeout_unit: str | None = None, + *, + rehearsal_time_to_complete: float | int | None = None, + ) -> "Wait": + """Create a wait from a random uniform time. + + If timeout_unit is specified, UPSTAGE will try to convert it to the + time_unit set in the stage. Otherwise, it defaults to that time unit. + + Args: + low (float): Lower bounds of random draw + high (float): Upper bounds of random draw + timeout_unit (str, optional): Units of time + rehearsal_time_to_complete (float | int, optional): The rehearsal time + to complete. Defaults to None - meaning the random value drawn. + + Returns: + Wait: The timeout event + """ + rng = UpstageBase().stage.random + timeout = rng.uniform(low, high) + return cls(timeout, timeout_unit, rehearsal_time_to_complete=rehearsal_time_to_complete) + + def as_event(self) -> SIM.Timeout: + """Cast Wait event as a simpy Timeout event. + + Returns: + SIM.Timeout + """ + assert isinstance(self.env, SIM.Environment) + if self._simpy_event is None: + self._simpy_event = self.env.timeout(self._time_to_complete) + return self._simpy_event + + def cancel(self) -> None: + """Cancel the timeout. + + There's no real meaning to cancelling a timeout. It sits in simpy's queue either way. + """ + assert self._simpy_event is not None + try: + self._simpy_event.defused = True + except RuntimeError as exc: + warn(f"Runtime error when cancelling '{self}', Error: {exc}!") + + +class All(MultiEvent): + """An event that requires all events to succeed before succeeding.""" + + @staticmethod + def simpy_equivalent(env: SIM.Environment, events: list[SIM.Event]) -> SIM.Event: + """Return the SimPy version of the UPSTAGE All event. + + Args: + env (SIM.Environment): SimPy Environment. + events (list[SIM.Event]): List of events. + + Returns: + SIM.Event: A simpy AllOf event. + """ + return SIM.AllOf(env, events) + + +class BaseRequestEvent(BaseEvent): + """Base class for Request Events. + + Requests are things like Get and Put that wait in a queue. + """ + + def __init__(self, rehearsal_time_to_complete: float = 0.0) -> None: + """Create a request event. + + Args: + rehearsal_time_to_complete (float, optional): Estimated time to complete. + Defaults to 0.0. + """ + super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) + self._simpy_event: SIM_REQ_EVTS | None = None + + def cancel(self) -> None: + """Cancel the Request.""" + if self._simpy_event is None: + return + if not self.is_complete(): + self._simpy_event.cancel() + # Note: inherited classes need to deal with put-backs. + + +class Put(BaseRequestEvent): + """Wrap the ``simpy`` Put event. + + This is an event that puts an object into a ``simpy`` store or puts + an amount into a container. + + """ + + def __init__( + self, + put_location: SIM.Container | SIM.Store, + put_object: float | int | tyAny, + rehearsal_time_to_complete: float = 0.0, + ) -> None: + """Create a Put request for a store or container. + + Args: + put_location (SIM.Container | SIM.Store): Any container, store, or subclass. + put_object (float | int | Any): The amount (float | int) or object (Any) to put. + rehearsal_time_to_complete (float, optional): Estimated time for the put to finish. + Defaults to 0.0. + """ + super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) + + if not issubclass(put_location.__class__, SIM.Container | SIM.Store): + raise SimulationError( + f"put_location must be a subclass of Container " + f"or Store, not {put_location.__class__}" + ) + + self.put_location = put_location + self.put_object = put_object + self._simpy_event: ContainerPut | StorePut | None = None + + def as_event(self) -> ContainerPut | StorePut: + """Convert event to a ``simpy`` Event. + + Returns: + --------- + :obj:`simpy.events.Event` + Put request as a simpy event. + + """ + if self._simpy_event is None: + self._simpy_event = self.put_location.put(self.put_object) + return self._simpy_event + + +class Get(BaseRequestEvent): + """Wrap the ``simpy`` Get event. + + Event that gets an object from a ``simpy`` store or gets an amount from a + container. + """ + + def __init__( + self, + get_location: SIM.Store | SIM.Container, + *get_args: tyAny, + rehearsal_time_to_complete: float = 0.0, + **get_kwargs: tyAny, + ) -> None: + """Create a Get request on a store, container, or subclass of those. + + Args: + get_location (SIM.Store | SIM.Container): The place for the Get request + rehearsal_time_to_complete (float, optional): _description_. Defaults to 0.0. + get_args (Any): optional positional args for the get request + (blank for Store, for container it will be the amount) + get_kwargs (Any): optional keyword args for the get request + (blank for Store and Container, other kinds may have more) + """ + super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) + + if not issubclass(get_location.__class__, SIM.Container | SIM.Store): + raise SimulationError( + "'put_location' must be a subclass of Container" + f" or Store, not {get_location.__class__}" + ) + + self.get_location = get_location + self.get_args = get_args + self.get_kwargs = get_kwargs + self._simpy_event: ContainerGet | StoreGet | None = None + + def as_event(self) -> ContainerGet | StoreGet: + """Convert get to a ``simpy`` Event. + + Returns: + ContainerGet | StoreGet + """ + # TODO: optional checking for container types for feasibility + if self._simpy_event is None: + self._simpy_event = self.get_location.get( + *self.get_args, + **self.get_kwargs, + ) + return self._simpy_event + + def get_value(self) -> tyAny: + """Get the value returned when the request is complete. + + Returns: + Any: The amount or item requested. + """ + if isinstance(self._simpy_event, StoreGet): + try: + return self._simpy_event.value + except AttributeError: + raise SimulationError("Requested item from an unfinished Get request.") + elif isinstance(self._simpy_event, ContainerGet): + try: + return self._simpy_event.amount + except AttributeError: + raise SimulationError("Requested item from an unfinished Get request.") + else: + raise SimulationError("Requested item from an unfinished Get request.") + + def cancel(self) -> None: + """Cancel the get, and check if we got the item. + + There is an edge case where a Get request has the item, but + isn't given back to the process because an interrupt sorts + to first in the queue. This method handles that edge + case, giving the item back. + """ + super().cancel() + # Return the item if we got it. + if isinstance(self._simpy_event, ContainerGet | StoreGet): + with suppress(SimulationError): + value = self.get_value() + + def _putter() -> SIMPY_GEN: + yield self.get_location.put(value) + + self.env.process(_putter()) + + +class FilterGet(Get): + """A Get for a FilterStore.""" + + def __init__( + self, + get_location: SIM.FilterStore, + filter: Callable[[tyAny], bool], + rehearsal_time_to_complete: float = 0.0, + ) -> None: + """Create a Get request on a FilterStore. + + The filter function returns a boolean (in/out of consideration). + + Args: + get_location (SIM.Store | SIM.Container): The place for the Get request + filter (Callable[[Any], bool]): The function that filters items in the store + rehearsal_time_to_complete (float, optional): _description_. Defaults to 0.0. + """ + super().__init__( + get_location=get_location, + rehearsal_time_to_complete=rehearsal_time_to_complete, + filter=filter, + ) + + +class ResourceHold(BaseRequestEvent): + """Wrap the ``simpy`` request resource event. + + This manages getting and giving back all in one object. + + Example: + >>> resource = simpy.Resource(env, capacity=1) + >>> hold = ResourceHold(resource) + >>> # yield on the hold to get it + >>> yield hold + >>> # now that you have it, do things.. + >>> # give it back + >>> yield hold + >>> ... + """ + + def __init__( + self, + resource: SIM.Resource, + *resource_args: tyAny, + rehearsal_time_to_complete: float = 0.0, + **resource_kwargs: tyAny, + ) -> None: + """Create an event to use twice to get and give back a resource. + + Args: + resource (SIM.Resource): The simpy resource object. + rehearsal_time_to_complete (float, optional): Expected time to wait to + get the resource. Defaults to 0.0. + *resource_args (Any): positional arguments to the resource + **resource_kwargs (Any): keyword arguments to the resource + """ + super().__init__(rehearsal_time_to_complete=rehearsal_time_to_complete) + + self.resource = resource + self.resource_args = resource_args + self.resource_kwargs = resource_kwargs + self._stage = "request" + self._simpy_event: Request | Release | None = None + + def as_event(self) -> Request | Release: + """Create the simpy event for the right state of Resource usage. + + Returns: + Request | Release: The simpy event. + """ + if self._stage == "request": + self._simpy_event = self.resource.request(*self.resource_args, **self.resource_kwargs) + self._stage = "release" + return self._simpy_event + elif self._stage == "release": + if not self._simpy_event or not self._simpy_event.processed: + raise SimulationError( + "Resource release requested when the " + "resource hasn't been given. Did you cancel?" + ) + assert isinstance(self._simpy_event, Request) + self._simpy_event = self.resource.release(self._simpy_event) + self._stage = "completed" + return self._simpy_event + raise UpstageError(f"Bad stage for Resource Hold: {self._stage}") diff --git a/src/upstage_des/geography/__init__.py b/src/upstage_des/geography/__init__.py new file mode 100644 index 0000000..77a1350 --- /dev/null +++ b/src/upstage_des/geography/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Geography utils.""" diff --git a/src/upstage_des/py.typed b/src/upstage_des/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/upstage_des/rehearsing.py b/src/upstage_des/rehearsing.py new file mode 100644 index 0000000..4ef790b --- /dev/null +++ b/src/upstage_des/rehearsing.py @@ -0,0 +1,8 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Helpers for doing rehearsal.""" + +... diff --git a/src/upstage_des/resources/__init__.py b/src/upstage_des/resources/__init__.py new file mode 100644 index 0000000..29d1b82 --- /dev/null +++ b/src/upstage_des/resources/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""New resource types.""" diff --git a/src/upstage_des/root_types.py b/src/upstage_des/root_types.py new file mode 100644 index 0000000..71d3391 --- /dev/null +++ b/src/upstage_des/root_types.py @@ -0,0 +1,22 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""A place for types that are seen across upstage features. + +Helps avoid circularity and if TYPE_CHECKING. +""" + +from typing import Any, TypedDict + +PLANNING_FACTOR_OBJECT = object() + + +class StateDataDict(TypedDict): + """Type hinting for state data storage required on an actor.""" + + value: Any + active_data: Any | None + is_active: bool | None + last_update: float | None diff --git a/src/upstage_des/states.py b/src/upstage_des/states.py new file mode 100644 index 0000000..f24b645 --- /dev/null +++ b/src/upstage_des/states.py @@ -0,0 +1,403 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""State descriptor for actor attributes with interception hooks.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Union, cast, get_args, get_origin + +from upstage_des.base import SimulationError + +if TYPE_CHECKING: + from upstage_des.actor import _BaseActor as Actor + + +def check_type(value: Any, annotation: Any) -> bool: + """Check if a value matches the given type annotation. + + Args: + value: The value to check + annotation: The type annotation to check against + + Returns: + True if the value matches the annotation, False otherwise + """ + origin = get_origin(annotation) + + if origin is None: + try: + return isinstance(value, annotation) + except TypeError: + # Some things, like TypedDict, would fail here. + return True + + if origin is Union: + return any(check_type(value, arg) for arg in get_args(annotation)) + + if origin in (list, dict, set, tuple): + return isinstance(value, origin) + + return True + + +class State[T]: + """A descriptor for actor state with interception hooks for get/set operations. + + States are class-level descriptors that manage per-instance values on Actors. + They provide hooks for recording, validation, and side effects during state changes. + + Usage: + >>> class MyActor(Actor): + ... fuel: float + ... position: tuple[float, float] = State(default_factor=lambda: (0.0, 0.0)) + + >>> actor = MyActor(position=(0.0, 0.0), fuel=100.0) + >>> actor.fuel = 200.0 + + The State descriptor is automatically created by the Actor class for all annotations. + You can call it explicitly to set its properties and behaviors + """ + + def __init__( + self, + *, + default: Any = None, + default_factory: Callable[[], Any] | None = None, + recording: bool = True, + validator: Callable[[Any, Any], None] | None = None, + type_check_each: bool = False, + validate_each: bool = True, + ) -> None: + """Create a State descriptor. + + Args: + default: Default value for the state. + default_factory: Factory function for default values (for mutable defaults). + recording: Whether to record state changes. + validator: Optional validator function that raises on invalid values. + type_check_each: Optional control for when to type check on a set. + validate_each: Optional control for when to validate on a set. + + Raises: + ValueError: If both default and default_factory are provided. + """ + if default is not None and default_factory is not None: + raise ValueError("Cannot specify both default and default_factory") + + self.name: str = "" + self._default = default + self._default_factory = default_factory + self._recording = recording + self._validator = validator + self._unchecked = True + self._given_type = ... + self._type_check_each = type_check_each + self._validate_each = validate_each + + def __get__(self, obj: Union["Actor", None], objtype: type) -> T: + """Get the state value from an actor instance. + + Args: + obj: The actor instance (None if accessed from class). + objtype: The actor class. + + Returns: + The State descriptor if accessed from class, otherwise the state value. + + Raises: + AttributeError: If the state has not been set and has no default. + """ + if obj is None: + raise SimulationError("State value access didn't have an actor.") + + value = obj._state_data[self.name]["value"] + + return value # type: ignore[no-any-return] + + def _record_change(self, obj: "Actor", new_value: Any) -> None: + """Record a state change. + + Args: + obj: The actor instance. + old_value: The previous value. + new_value: The new value. + """ + if hasattr(obj, "_record_state_change"): + obj._record_state_change(self.name, new_value) + + def __set__(self, obj: "Actor", value: T, no_record: bool = False) -> None: + """Set the state value on an actor instance. + + Args: + obj: The actor instance. + value: The new value to set. + no_record: If the state should record. + + Raises: + ValueError: If the validator rejects the value. + """ + do_validate = self._unchecked or self._validate_each + do_type = self._unchecked or self._type_check_each + self._unchecked = False + if do_validate and self._validator is not None: + self._validator(obj, value) + if do_type and not check_type(value, self._given_type): + raise TypeError(f"Input {value} for {self.name} doesn't match type {self._given_type}") + + obj._state_data[self.name]["value"] = value + + if self._recording and not no_record: + self._record_change(obj, value) + + def __set_name__(self, owner: type, name: str) -> None: + """Set the name of the state attribute. + + Called by Python when the descriptor is assigned to a class attribute. + + Args: + owner: The class that owns this descriptor. + name: The attribute name. + """ + self.name = name + + def _add_type(self, typing: Any) -> None: + self._given_type = typing + + def _set_default(self, obj: Any) -> None: + if self.has_default: + if self._default_factory is not None: + value = self._default_factory() + else: + value = self._default + self.__set__(obj, value) + else: + raise ValueError("No default given") + + @property + def has_default(self) -> bool: + """Whether this state has a default value. + + Returns: + True if default or default_factory is provided. + """ + return self._default is not None or self._default_factory is not None + + +class _ActiveState[T](State): + """A state meant to change over time.""" + + def __init__( + self, + *, + default: Any = None, + default_factory: Callable[[], Any] | None = None, + recording: bool = True, + validator: Callable[[Any, Any], None] | None = None, + type_check_each: bool = False, + validate_each: bool = True, + ) -> None: + """Create the state. + + Args: + default (Any, optional): _description_. Defaults to None. + default_factory (Callable[[], Any] | None, optional): _description_. + Defaults to None. + recording (bool, optional): _description_. Defaults to True. + validator (Callable[[Any, Any], None] | None, optional): _description_. + Defaults to None. + type_check_each (bool, optional): _description_. Defaults to False. + validate_each (bool, optional): _description_. Defaults to True. + """ + super().__init__( + default=default, + default_factory=default_factory, + recording=recording, + validator=validator, + type_check_each=type_check_each, + validate_each=validate_each, + ) + + def __get__(self, obj: Union["Actor", None], objtype: type) -> T: + if obj is None: + raise SimulationError("Unexpected behavior in state value access") + # Activate states calculate on a get + update = obj._state_data[self.name].get("last_update") + is_active = obj._state_data[self.name].get("is_active") + if is_active and (update is None or update != obj.env.now): + obj._state_data[self.name]["last_update"] = obj.env.now + value = self.calculate_value(obj) + self.__set__(obj, value) + return value + else: + return cast(T, super().__get__(obj, objtype)) + + def calculate_value(self, obj: "Actor") -> T: + """Calculate the current value of the state. + + Args: + time (float): The time this is called. + obj (Any): Typically the actor. + + Returns: + T: The value + """ + raise NotImplementedError("Calling on a blank active state.") + + def predict_value_time(self, obj: "Actor", value: T) -> float | None: + """Get the time when the state will equal a value. + + This is an optional implementation to support making events for when + a state reaches a given value. + + Args: + obj (ActorLike): The actor + value (T): The goal value + + Returns: + float | None: The time from now when the state will be that value. + """ + return None + + def base_activate(self, obj: "Actor", curr_value: T, allow_active: bool = False) -> None: + """Activate the state. + + This should store data about the activation to be used on a __get__ to + update the value. + + When subclassing, this gets called after the data setup that goes in the + new `activate`. + + Args: + obj (Actor): The actor + curr_value (T): The current value of the state. + allow_active (bool, optional): Future proof to allow active states to + mutally activate. Defaults to False. + """ + # We can't be active already + is_active = obj._state_data[self.name].get("is_active", False) + if is_active and not allow_active: + raise SimulationError(f"{self.name} state is already active.") + # Get the value + obj._record_state_change(self.name, curr_value, "ACTIVATING") + obj._state_data[self.name]["is_active"] = True + obj._state_data[self.name]["last_update"] = obj.env.now + + def activate(self, obj: "Actor", **kwargs: Any) -> None: + """Activation function. + + Sets up the data and calls base_activate. + + Args: + obj (Actor): The actor + kwargs (Any): Arguments to activate. + """ + raise NotImplementedError("You must create an activation method.") + + def base_deactivate(self, obj: "Actor", last_value: T) -> None: + """Deactivate the state. + + A subclass should clean all the data out and supply this method with + the last value. + + Args: + obj (Actor): _description_ + last_value (T): _description_ + + Raises: + SimulationError: _description_ + """ + if not obj._state_data[self.name]["is_active"]: + raise SimulationError(f"{self.name} state is already deactivated.") + # do a final get + self.__set__(obj, last_value, no_record=True) + obj._record_state_change(self.name, last_value, "DEACTIVATING") + obj._state_data[self.name]["is_active"] = False + obj._state_data[self.name]["last_update"] = None + + def deactivate(self, obj: "Actor", **kwargs: Any) -> None: + """Dectivation function. + + Sets up the data and calls base_activate. + + Args: + obj (Actor): The actor + kwargs (Any): Arguments to deactivate. + """ + raise NotImplementedError("You must create a deactivation method.") + + +@dataclass +class _LinearData: + start: float + rate: float + value: float + + +class LinearChangingState(_ActiveState[float]): + """A state that changes linearly over time.""" + + def predict_value_time(self, obj: "Actor", value: float) -> float | None: + """Get the time when the state will equal a value. + + Args: + obj (ActorLike): The actor + value (T): The goal value + + Returns: + float | None: The time from now when the state will be that value. + """ + data: _LinearData | None = obj._state_data[self.name].get("active_data", None) + if data is None: + return None + # curr + rate * time = value + # rate * time = (value - curr) + # time = (value - curr) / rate + time = (value - data.value) / data.rate + reach_time = time + data.start + if reach_time > obj.env.now: + return reach_time + return None + + def calculate_value(self, obj: "Actor") -> float: + """Linear change calculation. + + Args: + obj (Actor): The actor. + + Returns: + float: The new value. + """ + time = obj.env.now + data: _LinearData | None = obj._state_data[self.name].get("active_data", None) + if data is None: + raise SimulationError(f"Data for state {self.name} not found.") + delta = time - data.start + move = delta * data.rate + return data.value + move + + def activate(self, obj: "Actor", rate: float = 0.0, **kwargs: Any) -> None: + """Activate the linear state, supplying a rate. + + Args: + obj (Actor): The actor + rate (float): The rate of change + kwargs (Any): Not used, just for typing. + """ + value = self.__get__(obj, obj.__class__) + data = _LinearData(start=obj.env.now, rate=rate, value=value) + obj._state_data[self.name]["active_data"] = data + self.base_activate(obj, value, allow_active=False) + + def deactivate(self, obj: "Actor", **kwargs: Any) -> None: + """Deactivate the state. + + Args: + obj (Actor): The actor. + kwargs (Any): Unused kwargs, just for typing. + """ + v = self.calculate_value(obj) + obj._state_data[self.name]["active_data"] = None + self.base_deactivate(obj, v) diff --git a/src/upstage_des/tasks.py b/src/upstage_des/tasks.py new file mode 100644 index 0000000..0254438 --- /dev/null +++ b/src/upstage_des/tasks.py @@ -0,0 +1,315 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Tasks constitute the actions that Actors can perform.""" + +from collections.abc import Generator +from enum import IntFlag +from typing import TYPE_CHECKING, Any, TypeVar +from warnings import warn + +from simpy import Environment as SimpyEnv +from simpy import Event as SimpyEvent +from simpy import Interrupt, Process + +from upstage_des.actor import Actor, ActorHelper +from upstage_des.base import SimulationError, UpstageBase, process +from upstage_des.events import BaseEvent, Event + +__all__ = ("DecisionTask", "Task", "process", "TerminalTask", "InterruptStates") + + +NOT_IMPLEMENTED_MSG = "User must define the actions performed when executing this task" + +TASK_GEN = Generator[BaseEvent, Any, None] + + +class InterruptStates(IntFlag): + """Class that describes how to behave after an interrupt.""" + + END = 0 + IGNORE = 1 + RESTART = 2 + + +EVT = TypeVar("EVT", bound=BaseEvent) + + +class Task(UpstageBase, ActorHelper): + """A Task is an action that can be performed by an Actor. + + Inherits ActorHelper to provide methods for tracing knowledge modification + and other features on the Actor. + """ + + def __init__(self) -> None: + """Create a task instance.""" + super().__init__() + self._marker: str | None = None + self._marked_time: float | None = None + self._interrupt_action: InterruptStates = InterruptStates.END + self._final_interrupt: bool = False + + def task(self, *, actor: Actor) -> TASK_GEN: + """Define the process this task follows.""" + raise NotImplementedError(NOT_IMPLEMENTED_MSG) + + def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: + """Define any actions to take on the actor if this task is interrupted. + + Note: + Custom Tasks can overwrite this method so they can handle being + interrupted with a custom procedure. By default, interrupt ends the + task. + + Args: + actor (Actor): the actor using the task + cause (Any): Optional data for the interrupt + """ + actor.write_to_log(f"Interrupted while performing {self}. Reasons: {cause}") + return self._interrupt_action + + def set_marker( + self, marker: str, interrupt_action: InterruptStates = InterruptStates.END + ) -> None: + """Set a marker to help with inspection of interrupts. + + The interrupt_action is set for when no `on_interrupt` is implemented. + + Args: + marker (str): String for the marker. + interrupt_action (InterruptStates, optional): Action to take on interrupt. + Defaults to InterruptStates.END. + """ + self._marker = marker + self._marked_time = self.env.now + self._interrupt_action = interrupt_action + + def get_marker(self) -> str | None: + """Get the current marker. + + Returns: + str | None: Marker (or None if cleared) + """ + return self._marker + + def get_marker_time(self) -> float | None: + """The time the current marker was set. + + Returns: + float | None: Marker set time (or None if cleared) + """ + return self._marked_time + + def clear_marker(self) -> None: + """Clear the marker and set that an interrupt ends the task.""" + self._marker = None + self._marked_time = None + self._interrupt_action = InterruptStates.END + + def _handle_interruption( + self, actor: Actor, interrupt: Interrupt, next_event: BaseEvent | Process + ) -> InterruptStates: + """Clean up after an interrupt and perform interrupt checks/actions. + + Args: + actor (Actor): _description_ + interrupt (Interrupt): _description_ + next_event (BaseEvent): _description_ + + Returns: + InterruptStates: action to take + """ + # test the interrupt behavior: + _interrupt_action = self.on_interrupt( + actor=actor, + cause=interrupt.cause, + ) + if _interrupt_action is None and not self._final_interrupt: + raise SimulationError("No interrupt behavior returned from `on_interrupt`") + + if ( + _interrupt_action in (InterruptStates.END, InterruptStates.RESTART) + or self._final_interrupt + ): + actor.write_to_log(f"Interrupted by {interrupt}.") + actor.deactivate_all_states(cause=self) + if isinstance(next_event, BaseEvent): + names = list(actor.knowledge.keys()) + for name in names: + if actor.knowledge[name] is next_event: + actor.clear_knowledge(name, caller=f"Clearing knowledge event {name}") + next_event.cancel() + elif isinstance(next_event, Process): + next_event.interrupt(cause=interrupt.cause) + else: + raise SimulationError(f"Bad event passed: {next_event}") + elif _interrupt_action != InterruptStates.IGNORE: + raise SimulationError(f"Wrong interrupt action value: {_interrupt_action}") + if self._final_interrupt: + _interrupt_action = InterruptStates.END + return _interrupt_action + + @process + def run(self, *, actor: Actor) -> Generator[SimpyEvent | Process, Any, None]: + """Execute the task. + + Args: + actor (Actor): The actor using the task + + Returns: + Generator[SimpyEvent, Any, None] + """ + generators = [ + self.task(actor=actor), + ] + return_item = None + stop_run = False + back_from_interrupt = False + event_to_yield: Process | SimpyEvent + while not stop_run: + try: + while True: + if not back_from_interrupt: + try: + if return_item is None: + next_event = next(generators[-1]) + else: + next_event = generators[-1].send(return_item) + except StopIteration as e: + generators.pop() + if e.value is not None: + return_item = e.value + if not generators: + stop_run = True + break + continue + + if isinstance(next_event, Process): + warn( + f"Yielding a simpy.Process from {self}. " + f"This is dangerous, take care. ", + UserWarning, + ) + event_to_yield = next_event + elif isinstance(next_event, BaseEvent): + event_to_yield = next_event.as_event() + else: + raise SimulationError(f"Unexpected yielded event type: {next_event}") + else: + back_from_interrupt = False + return_item = yield event_to_yield + # TODO: test if the return_item is for a multi-event + # that way we can return it as a more useful object + + except Interrupt as interrupt: + action = self._handle_interruption( + actor, + interrupt, + next_event, + ) + if action == InterruptStates.IGNORE: + back_from_interrupt = True + else: + # This is restart or end; either way we have to cancel + # any routines in the stack. + while generators: + gen = generators.pop() + gen.close() + if action == InterruptStates.RESTART: + generators = [ + self.task(actor=actor), + ] + return_item = None + else: + stop_run = True + + +class DecisionTask(Task): + """A task used for zero-time decision making.""" + + DO_NOT_HOLD: bool = False + + def task(self, *, actor: Actor) -> TASK_GEN: + """Define the process this task follows.""" + raise SimulationError("No need to call `task` on a DecisionTask") + + def make_decision(self, *, actor: Actor) -> None: + """Define the process this task follows.""" + raise NotImplementedError(NOT_IMPLEMENTED_MSG) + + @process + def run(self, *, actor: Actor) -> Generator[SimpyEvent, None, None]: + """Run the decision task. + + Args: + actor (Actor): The actor making decisions + + Yields: + Generator[SimpyEvent, None, None]: Generator for SimPy event queue. + """ + self.make_decision(actor=actor) + assert isinstance(self.env, SimpyEnv) + yield self.env.timeout(0.0) + + def run_skip(self, *, actor: "Actor") -> None: + """Run the decision task with no clock reference. + + Task networks will use this method if DO_NOT_HOLD is True. + + Args: + actor (Actor): The actor making decisions + """ + self.make_decision(actor=actor) + + +class TerminalTask(Task): + """A task that cannot exit, i.e., it is terminal. + + Note: + The user can re-implement the `log_message` method to return a custom + message that will be appended to the actor's log through its `log` + method. + """ + + _time_to_complete: float = 1e24 + + def log_message(self, *, actor: Actor) -> str: + """A message to save to a log when this task is reached. + + Args: + actor (Actor): The actor using this task. + + Returns: + str: A log message + """ + return f"Entering terminal task: {self}" + + def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: + """Special case interrupt for terminal task. + + Args: + actor (Actor): The actor + cause (Any): Additional data sent to the interrupt. + """ + if not self._final_interrupt: + raise SimulationError( + f"Cannot interrupt a terminal task {self} on {actor}. Kwargs sent: {cause}" + ) + return InterruptStates.END + + def task(self, *, actor: Actor) -> TASK_GEN: + """The terminal task. + + It's just a long wait. + + Args: + actor (Actor): The actor + """ + log_message = self.log_message(actor=actor) + actor.write_to_log(log_message) + the_long_event = Event(rehearsal_time_to_complete=self._time_to_complete) + yield the_long_event + raise SimulationError(f"A terminal task completed on {actor}") diff --git a/src/upstage_des/units.py b/src/upstage_des/units.py new file mode 100644 index 0000000..0a9c89c --- /dev/null +++ b/src/upstage_des/units.py @@ -0,0 +1,92 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Unit conversion help.""" + +_distance_to_m = { + "mm": 0.001, + "cm": 0.01, + "m": 1.0, + "meter": 1.0, + "meters": 1.0, + "km": 1000.0, + "inch": 0.0254, + "inches": 0.0254, + "in": 0.0254, + "ft": 0.3048, + "feet": 0.3048, + "foot": 0.3048, + "yd": 0.9144, + "yard": 0.9144, + "yards": 0.9144, + "mile": 1609.344, + "miles": 1609.344, + "mi": 1609.344, + "nmi": 1852.0, +} + +_time_to_s = { + "ns": 1e-9, + "us": 1e-6, + "μs": 1e-6, + "ms": 0.001, + "s": 1.0, + "sec": 1.0, + "second": 1.0, + "seconds": 1.0, + "min": 60.0, + "minute": 60.0, + "minutes": 60.0, + "h": 3600.0, + "hr": 3600.0, + "hour": 3600.0, + "hours": 3600.0, + "day": 86400.0, + "days": 86400.0, +} + + +def unit_convert(value: float, from_unit: str, to_unit: str) -> float: + """Convert between distance and time units. + + Supported distance units: + mm, cm, m, meter, meters, km, inch, inches, in, + ft, feet, foot, yd, yard, yards, mile, miles, mi, nmi + + Supported time units: + ns, us, μs, ms, s, sec, seconds, min, minute, minutes, + h, hr, hour, hours, day, days + + Example: + unit_convert(100, 'km', 'mile') # -> 62.1371... + unit_convert(3600, 's', 'h') # -> 1.0 + """ + # Normalize input (handle case and common variations) + from_unit = from_unit.lower().strip() + to_unit = to_unit.lower().strip() + + if from_unit in _distance_to_m and to_unit in _distance_to_m: + meters = value * _distance_to_m[from_unit] + return meters / _distance_to_m[to_unit] + elif from_unit in _time_to_s and to_unit in _time_to_s: + seconds = value * _time_to_s[from_unit] + return seconds / _time_to_s[to_unit] + else: + raise ValueError( + f"Cannot convert from '{from_unit}' to '{to_unit}'. " + f"Both units must be either distance or time." + ) + + +def speed_convert(value: float, dist1: str, time1: str, dist2: str, time2: str) -> float: + """Convert between speeds. + + Example: + speed_convert(60, "miles", "hour", "km", "sec") # -> 0.0268224 + speed_convert(1, "meters", "second", "nmi", "hr") # -> 1.98 + """ + d = unit_convert(1, dist1, dist2) + t = unit_convert(1, time1, time2) + return value * d / t diff --git a/tests/test_active_state.py b/tests/test_active_state.py new file mode 100644 index 0000000..0e0ce78 --- /dev/null +++ b/tests/test_active_state.py @@ -0,0 +1,239 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test the active state""" + +from collections import deque + +import pytest +from upstage_des.actor import Actor +from upstage_des.states import LinearChangingState +from upstage_des.base import EnvironmentContext, SimulationError + + +def test_linear_state() -> None: + class MyActor(Actor): + rate_state: float + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor( + name="Rated", + rate_state=2.0, + level=0.0, + ) + env.run(1.0) + act.level=1. + env.run(1.5) + act.activate_linear_state("level", act.rate_state, cause="TEST") + env.run(2.5) + assert act.level == 3.0 + assert act._state_histories["level"] == deque([ + (0.0, 0.0), + (1.0, 1.0), + (1.5, 1.0, "ACTIVATING"), + (2.5, 3.0), + ]) + env.run(3.0) + act.deactivate_state("level", cause="TEST") + env.run(4.0) + assert act.level == 4.0 + env.run(5.0) + act.level = 6.2 + assert act._state_histories["level"][4] == (3.0, 4.0, "DEACTIVATING") + assert act._state_histories["level"][5] == (5.0, 6.2) + assert len(act._state_histories["level"]) == 6 + + +def test_linear_predict() -> None: + class MyActor(Actor): + rate_state: float + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor( + name="Rated", + rate_state=2.0, + level=0.0, + ) + field = act.__model_fields__["level"] + assert isinstance(field, LinearChangingState) + assert field.predict_value_time(act, 6.4) is None + env.run(0.5) + act.activate_linear_state("level", rate=3.2, cause="TEST") + assert field.predict_value_time(act, 6.4) == 2.5 + env.run(1.5) + assert field.predict_value_time(act, 6.4) == 2.5 + env.run(3.5) + assert field.predict_value_time(act, 6.4) is None + act.deactivate_linear_state("level", cause="TEST") + act.activate_linear_state("level", rate=-3.0, cause="TEST") + assert field.predict_value_time(act, 6.4) is not None + + +def test_linear_errors() -> None: + class MyActor(Actor): + rate_state: float + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor( + name="Rated", + rate_state=2.0, + level=0.0, + ) + with pytest.raises(SimulationError): + act.deactivate_linear_state("level", cause="TEST") + act.activate_linear_state("level", 1.3, cause="TEST") + with pytest.raises(SimulationError): + act.activate_state("level", rate=3.4, cause="TEST") + + +def test_linear_negative_rate() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Drainer", level=100.0) + env.run(1.0) + act.activate_linear_state("level", rate=-5.0, cause="TEST") + env.run(3.0) + assert act.level == 90.0 + env.run(5.0) + assert act.level == 80.0 + assert act._state_histories["level"] == deque([ + (0.0, 100.0), + (1.0, 100.0, "ACTIVATING"), + (3.0, 90.0), + (5.0, 80.0), + ]) + + +def test_linear_multiple_activations() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Changer", level=0.0) + act.activate_linear_state("level", rate=2.0, cause="TEST") + env.run(2.0) + assert act.level == 4.0 + act.deactivate_linear_state("level", cause="TEST") + env.run(4.0) + assert act.level == 4.0 + act.activate_linear_state("level", rate=3.0, cause="TEST") + env.run(6.0) + assert act.level == 10.0 + + +def test_linear_zero_rate() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Static", level=50.0) + act.activate_linear_state("level", rate=0.0, cause="TEST") + env.run(10.0) + assert act.level == 50.0 + env.run(20.0) + assert act.level == 50.0 + + +def test_linear_predict_negative_rate() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Predictor", level=100.0) + act.activate_linear_state("level", rate=-2.0, cause="TEST") + field = act.__model_fields__["level"] + assert isinstance(field, LinearChangingState) + env.run(5.0) + assert field.predict_value_time(act, 80.0) == 10.0 + assert field.predict_value_time(act, 50.0) == 25.0 + + +def test_linear_predict_past_value() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Predictor", level=10.0) + act.activate_linear_state("level", rate=5.0, cause="TEST") + env.run(2.0) + field = act.__model_fields__["level"] + assert isinstance(field, LinearChangingState) + assert field.predict_value_time(act, 5.0) is None + + +def test_linear_state_history_deactivate() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Tracker", level=10.0) + env.run(1.0) + act.activate_linear_state("level", rate=5.0, cause="TEST") + env.run(3.0) + act.deactivate_linear_state("level", cause="TEST") + history = act._state_histories["level"] + assert history[0] == (0.0, 10.0) + assert history[1] == (1.0, 10.0, "ACTIVATING") + assert history[2] == (3.0, 20.0, "DEACTIVATING") + + +def test_linear_get_updates_value() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Getter", level=0.0) + act.activate_linear_state("level", rate=10.0, cause="TEST") + env.run(1.0) + val1 = act.level + val2 = act.level + assert val1 == 10.0 + assert val2 == 10.0 + assert len(act._state_histories["level"]) == 3 + + +def test_linear_state_recording_false() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=False) + + with EnvironmentContext() as env: + act = MyActor(name="Untracked", level=5.0) + act.activate_linear_state("level", rate=2.0, cause="TEST") + env.run(3.0) + assert act.level == 11.0 + assert act._state_histories["level"] == deque([(0.0, 5.0, "ACTIVATING")]) + + +def test_make_event_for_state_goal() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Tester", level=10.0) + assert act.make_event_for_state_goal("level", 50.0) is None + act.activate_linear_state("level", rate=5.0, cause="TEST") + env.run(2.0) + goal_time = act.make_event_for_state_goal("level", 50.0) + assert goal_time == 8.0 + env.run(goal_time) + assert act.level == 50.0 + + +def test_make_event_for_state_goal_negative_rate() -> None: + class MyActor(Actor): + level: float = LinearChangingState(recording=True) + + with EnvironmentContext() as env: + act = MyActor(name="Drainer", level=100.0) + act.activate_linear_state("level", rate=-10.0, cause="TEST") + goal_time = act.make_event_for_state_goal("level", 50.0) + assert goal_time == 5.0 + env.run(goal_time) + assert act.level == 50.0 diff --git a/tests/test_actor_clone.py b/tests/test_actor_clone.py new file mode 100644 index 0000000..1806688 --- /dev/null +++ b/tests/test_actor_clone.py @@ -0,0 +1,145 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test cloning actors.""" + +from upstage_des import Actor, EnvironmentContext, get_entities_by_class + + +def test_actor_clone_basic() -> None: + class Vehicle(Actor): + fuel: float = 100.0 + position: int = 0 + + with EnvironmentContext(): + vehicle = Vehicle(name="car1", fuel=50.0, position=10) + + cloned = vehicle.clone() + + assert cloned.name == "car1" + assert cloned.fuel == 50.0 + assert cloned.position == 10 + assert cloned.is_clone is True + assert vehicle.is_clone is False + + +def test_actor_clone_deepcopy() -> None: + class Robot(Actor): + inventory: list[str] + position: tuple[int, int] + + with EnvironmentContext(): + robot = Robot(name="bot1", inventory=["item1", "item2"], position=(5, 10)) + + cloned = robot.clone() + + assert cloned.inventory == ["item1", "item2"] + assert cloned.inventory is not robot.inventory + + cloned.inventory.append("item3") + assert len(robot.inventory) == 2 + assert len(cloned.inventory) == 3 + robot._clean() + assert len(cloned.inventory) == 3 + + assert cloned.position == (5, 10) + + +def test_actor_clone_no_history() -> None: + class Ship(Actor): + speed: float = 0.0 + + with EnvironmentContext() as env: + ship = Ship(name="ship1", speed=10.0) + + assert "speed" in ship._state_histories + assert len(ship._state_histories["speed"]) == 1 + + ship.speed = 20.0 + assert len(ship._state_histories["speed"]) == 2 + + cloned = ship.clone() + + assert cloned.speed == 20.0 + assert len(cloned._state_histories) == 0 + + +def test_actor_clone_not_in_registry() -> None: + class Drone(Actor): + altitude: float = 0.0 + + with EnvironmentContext(): + drone = Drone(name="drone1", altitude=100.0) + + entities = get_entities_by_class("Drone") + assert len(entities) == 1 + assert drone in entities + + cloned = drone.clone() + + entities_after = get_entities_by_class("Drone") + assert len(entities_after) == 2 + assert drone in entities_after + assert cloned in entities_after + + +def test_actor_clone_inheritance() -> None: + class Vehicle(Actor): + fuel: float = 100.0 + + class Car(Vehicle): + passengers: int = 0 + + with EnvironmentContext(): + car = Car(name="sedan", fuel=75.0, passengers=3) + + cloned = car.clone() + + assert cloned.name == "sedan" + assert cloned.fuel == 75.0 + assert cloned.passengers == 3 + assert cloned.is_clone is True + assert isinstance(cloned, Car) + assert isinstance(cloned, Vehicle) + + +def test_actor_clone_modified_after_creation() -> None: + class Tank(Actor): + ammo: int = 100 + health: float = 100.0 + + with EnvironmentContext() as env: + tank = Tank(name="tank1", ammo=50, health=75.0) + + env.run(env.timeout(5)) + tank.ammo = 25 + tank.health = 50.0 + + cloned = tank.clone() + + assert cloned.ammo == 25 + assert cloned.health == 50.0 + assert cloned.is_clone is True + + cloned.ammo = 100 + assert tank.ammo == 25 + assert cloned.ammo == 100 + + +def test_actor_clone_complex_nested_state() -> None: + class Agent(Actor): + data: dict[str, list[int]] + + with EnvironmentContext(): + agent = Agent(name="agent1", data={"scores": [1, 2, 3], "levels": [10, 20]}) + + cloned = agent.clone() + + assert cloned.data == {"scores": [1, 2, 3], "levels": [10, 20]} + assert cloned.data is not agent.data + + cloned.data["scores"].append(4) + assert agent.data["scores"] == [1, 2, 3] + assert cloned.data["scores"] == [1, 2, 3, 4] diff --git a/tests/test_actor_state.py b/tests/test_actor_state.py new file mode 100644 index 0000000..7291ca5 --- /dev/null +++ b/tests/test_actor_state.py @@ -0,0 +1,174 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test state features.""" + +import pytest + +from upstage_des.actor import Actor +from upstage_des.base import EnvironmentContext +from upstage_des.states import State + + +def test_basic_state_creation() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor = MyActor(name="test", fuel=100.0) + assert actor.fuel == 100.0 + + +def test_state_with_default() -> None: + class MyActor(Actor): + fuel: float = 50.0 + + with EnvironmentContext(): + actor = MyActor(name="test") + assert actor.fuel == 50.0 + + +def test_state_modification() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor = MyActor(name="test", fuel=100.0) + actor.fuel = 75.0 + assert actor.fuel == 75.0 + + +def test_name_field() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor = MyActor(name="car", fuel=100.0) + assert actor.name == "car" + assert actor.fuel == 100.0 + + +def test_name_field_inherited() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor = MyActor(name="default_name", fuel=100.0) + assert actor.name == "default_name" + + +def test_inheritance_basic() -> None: + class Vehicle(Actor): + fuel: float + + class Car(Vehicle): + passengers: int + + with EnvironmentContext(): + car = Car(name="sedan", fuel=100.0, passengers=4) + assert car.name == "sedan" + assert car.fuel == 100.0 + assert car.passengers == 4 + + +def test_state_validator() -> None: + def validate_positive(obj: object, value: float) -> None: + if value < 0: + raise ValueError("Must be positive") + + class MyActor(Actor): + fuel: float = State(default=100.0, validator=validate_positive) + + with EnvironmentContext(): + actor = MyActor(name="test") + actor.fuel = 50.0 + + with pytest.raises(ValueError, match="Must be positive"): + actor.fuel = -10.0 + + +def test_state_default_factory() -> None: + class MyActor(Actor): + items: list[int] = State(default_factory=list) + + with EnvironmentContext(): + actor1 = MyActor(name="test1") + actor2 = MyActor(name="test2") + + actor1.items.append(1) + assert len(actor1.items) == 1 + assert len(actor2.items) == 0 + + +def test_mixed_fields() -> None: + class MyActor(Actor): + fuel: float + position: tuple[float, float] = (0.0, 0.0) + + with EnvironmentContext(): + actor = MyActor(name="vehicle", fuel=100.0) + assert actor.name == "vehicle" + assert actor.fuel == 100.0 + assert actor.position == (0.0, 0.0) + + +def test_multiple_instances_independent() -> None: + class MyActor(Actor): + fuel: float | int = 100.0 + + with EnvironmentContext(): + actor1 = MyActor(name="test1") + actor2 = MyActor(name="test2", fuel=50.0) + + assert actor1.fuel == 100.0 + assert actor2.fuel == 50.0 + + actor1.fuel = 75.0 + assert actor1.fuel == 75.0 + assert actor2.fuel == 50.0 + + +def test_bad_type() -> None: + class MyActor(Actor): + fuel: float + position: tuple[float, float] = (0.0, 0.0) + + with EnvironmentContext(): + with pytest.raises(TypeError, match="doesn't match type"): + MyActor(name="vehicle", fuel=100) + + +def test_recording() -> None: + class MyActor(Actor): + fuel: float | int = 100.0 + + with EnvironmentContext() as env: + actor1 = MyActor(name="test1") + assert actor1._state_histories["fuel"][0] == (0.0, 100.0) + assert len(actor1._state_histories) == 1 + actor1.fuel = 200 + assert actor1._state_histories["fuel"][1] == (0.0, 200.0) + assert len(actor1._state_histories["fuel"]) == 2 + env.run(until=2) + actor1.fuel = 231.2 + assert actor1._state_histories["fuel"][2] == (2.0, 231.2) + + +def test_post_init() -> None: + var = [] + class MyActor(Actor): + def __post_init__(self) -> None: + var.append(self.name) + + class Next(MyActor): + def __post_init__(self) -> None: + super().__post_init__() + print('ok') + + with EnvironmentContext(): + MyActor(name="test") + assert len(var) == 1 + Next(name="test2") + assert var == ["test", "test2"] diff --git a/tests/test_entity_registry.py b/tests/test_entity_registry.py new file mode 100644 index 0000000..a7f72d0 --- /dev/null +++ b/tests/test_entity_registry.py @@ -0,0 +1,117 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test the entity registration.""" + +from upstage_des.actor import Actor +from upstage_des.base import EnvironmentContext, UpstageBase, get_entities_by_class, get_entity_registry + + +def test_entity_registry_basic() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor1 = MyActor(name="test1", fuel=100.0) + actor2 = MyActor(name="test2", fuel=50.0) + + registry = get_entity_registry() + assert "MyActor" in registry + assert len(registry["MyActor"]) == 2 + assert actor1 in registry["MyActor"] + assert actor2 in registry["MyActor"] + + +def test_entity_registry_inheritance() -> None: + class Vehicle(Actor): + fuel: float + + class Car(Vehicle): + passengers: int + + with EnvironmentContext(): + car = Car(name="sedan", fuel=100.0, passengers=4) + + registry = get_entity_registry() + assert "Car" in registry + assert "Vehicle" in registry + assert "Actor" in registry + assert "UpstageBase" in registry + + assert car in registry["Car"] + assert car in registry["Vehicle"] + assert car in registry["Actor"] + assert car in registry["UpstageBase"] + + +def test_get_entities_by_class() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor1 = MyActor(name="test1", fuel=100.0) + actor2 = MyActor(name="test2", fuel=50.0) + + actors = get_entities_by_class("MyActor") + assert len(actors) == 2 + assert actor1 in actors + assert actor2 in actors + + +def test_entity_registry_property() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor = MyActor(name="test", fuel=100.0) + + registry = actor.entity_registry + assert "MyActor" in registry + assert actor in registry["MyActor"] + + +def test_multiple_classes() -> None: + class Vehicle(Actor): + fuel: float + + class Building(UpstageBase): + pass + + class BigBuilding(Actor, Building): + pass + + with EnvironmentContext(): + vehicle = Vehicle(name="car", fuel=100.0) + building = Building() + big = BigBuilding(name="here") + + registry = get_entity_registry() + assert "Vehicle" in registry + assert "Building" in registry + assert "BigBuilding" in registry + assert vehicle in registry["Vehicle"] + assert building in registry["Building"] + assert big in registry["Building"] + + +def test_empty_registry() -> None: + with EnvironmentContext(): + registry = get_entity_registry() + assert isinstance(registry, dict) + assert len(registry) == 0 + + +def test_no_duplicates() -> None: + class MyActor(Actor): + fuel: float + + with EnvironmentContext(): + actor = MyActor(name="test", fuel=100.0) + + registry = get_entity_registry() + assert len(registry["MyActor"]) == 1 + + actor._register_entity() + assert len(registry["MyActor"]) == 1 diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..14478ea --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,558 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test the upstage events.""" + +import pytest +import simpy as SIM +from simpy.resources import base +from simpy.resources.container import ContainerGet, ContainerPut +from simpy.resources.store import StoreGet, StorePut + +from upstage_des import ( + EnvironmentContext, + SimulationError, +) +from upstage_des.events import ( + All, + Any, + BaseEvent, + BaseRequestEvent, + Event, + FilterGet, + Get, + Put, + ResourceHold, + Wait, +) +from upstage_des.base import SIMPY_GEN, Stage + + +def test_base_event() -> None: + init_time = 1.23 + with EnvironmentContext(initial_time=init_time) as env: + base = BaseEvent() + assert base.created_at == init_time, "Problem in environment time being stored in event" + assert base.env is env, "Problem in environment being stored in event" + + with pytest.raises(NotImplementedError): + base.as_event() + + +def test_wait_event() -> None: + init_time = 1.23 + with EnvironmentContext(initial_time=init_time) as env: + timeout = 1 + + wait = Wait(timeout=timeout) + assert wait.created_at == init_time, "Problem in environment time being stored in event" + assert wait.env is env, "Problem in environment being stored in event" + assert wait.timeout == timeout + + ret = wait.as_event() + assert isinstance(ret, SIM.Timeout), "Wait doesn't return a simpy timeout" + assert ret._delay == timeout, "Incorrect timeout time" + + stage = Stage(time_unit="minutes") + with EnvironmentContext(stage=stage) as env: + wait = Wait(timeout=1.1, timeout_unit="hours") + assert wait.timeout == pytest.approx(66) + + with EnvironmentContext(initial_time=init_time) as env: + timeout_2 = [1, 3] + wait = Wait.from_random_uniform(timeout_2[0], timeout_2[1]) + ret = wait.as_event() + assert isinstance(ret, SIM.Timeout), "Wait doesn't return a simpy timeout" + assert timeout_2[0] <= ret._delay <= timeout_2[1], "Incorrect timeout time" + + with pytest.raises(SimulationError): + Wait(timeout={1, 2}) # type: ignore [arg-type] + + with pytest.raises(SimulationError): + Wait(timeout="1") # type: ignore [arg-type] + + with pytest.raises(SimulationError): + Wait(timeout=[1]) # type: ignore [arg-type] + + with pytest.raises(SimulationError): + Wait(timeout=[1, 2, 3]) # type: ignore [arg-type] + + +def test_base_request_event() -> None: + init_time = 1.23 + with EnvironmentContext(initial_time=init_time) as env: + base = BaseRequestEvent() + assert base.created_at == init_time, "Problem in environment time being stored in event" + assert base.env is env, "Problem in environment being stored in event" + + base.cancel() + + +def test_put_event_with_stores() -> None: + with EnvironmentContext() as env: + store = SIM.Store(env, capacity=1) + put_object = ("A Test Object", 1.0) + put_event = Put(store, put_object) + + assert put_event.rehearsal_time_to_complete == 0.0, "Incorrect time to complete" + returned_object = put_event.as_event() + assert issubclass(returned_object.__class__, base.Put), ( + "Event returned is not simpy put event" + ) + env.run() + assert isinstance(returned_object, StorePut) + assert returned_object.item is put_object, "Wrong object put" + assert put_object in store.items + + put_object = ("A Second Test Object", 2.0) + put_event = Put(store, put_object) + event = put_event.as_event() + env.run() + assert not event.triggered, "Event shouldn't have completed" + assert event in store.put_queue, "Event is not waiting" + put_event.cancel() + env.run() + assert not event.triggered, "Event shouldn't have completed" + assert event not in store.put_queue, "Event is still in the store's queue" + + +def test_put_event_with_containers() -> None: + with EnvironmentContext() as env: + container = SIM.Container(env, capacity=1) + put_arg = 1.0 + put_event = Put(container, put_arg) + + assert put_event.rehearsal_time_to_complete == 0.0, "Incorrect time to complete" + returned_object = put_event.as_event() + assert issubclass(returned_object.__class__, base.Put), ( + "Event returned is not simpy put event" + ) + env.run() + assert isinstance(returned_object, ContainerPut) + assert returned_object.amount == put_arg, "Wrong amount put" + assert container.level == put_arg + + put_arg = 2 + put_event = Put(container, put_arg) + event = put_event.as_event() + env.run() + assert not event.triggered, "Event shouldn't have completed" + assert event in container.put_queue, "Event is not waiting" + put_event.cancel() + env.run() + assert not event.triggered, "Event shouldn't have completed" + assert event not in container.put_queue, "Event is still in the store's queue" + + +def test_get_event_with_stores() -> None: + with EnvironmentContext() as env: + store = SIM.Store(env, capacity=1) + put_object = ("A Test Object", 1.0) + store.put(put_object) + env.run() + + event = Get(store) + assert event.rehearsal_time_to_complete == 0.0, "Incorrect time to complete" + returned_object = event.as_event() + assert issubclass(returned_object.__class__, base.Get), ( + "Event returned is not simpy put event" + ) + + env.run() + assert isinstance(event._simpy_event, StoreGet) + item = event._simpy_event.value + assert item is put_object, "Returned item is not the original item" + item2 = event.get_value() + assert item is item2, "Same object from both methods" + + event = Get(store) + returned_object = event.as_event() + env.run() + assert returned_object in store.get_queue, "Event not in queue" + event.cancel() + assert returned_object not in store.get_queue, "Event is still in queue" + + +def test_get_event_with_containers() -> None: + with EnvironmentContext() as env: + container = SIM.Container(env, capacity=1) + put_arg = 1.0 + container.put(put_arg) + env.run() + + get_arg = 1.0 + event = Get(container, get_arg) + assert event.rehearsal_time_to_complete == 0.0, "Incorrect time to complete" + returned_object = event.as_event() + assert issubclass(returned_object.__class__, base.Get), ( + "Event returned is not simpy put event" + ) + + env.run() + assert isinstance(event._simpy_event, ContainerGet) + amount = event._simpy_event.amount + assert amount == get_arg, "Returned item is not the original item" + assert amount == event.get_value() + + event = Get(container, get_arg) + returned_object = event.as_event() + env.run() + assert returned_object in container.get_queue, "Event not in queue" + assert container.level == 0 + event.cancel() + assert returned_object not in container.get_queue, "Event is still in queue" + env.run() + assert container.level == 1 + + +def test_resource_events() -> None: + with EnvironmentContext() as env: + a_resource = SIM.Resource(env, capacity=1) + + request_object = ResourceHold(a_resource) + assert request_object._stage == "request", "Request object in wrong state" + request_object.as_event() + env.run() + + assert request_object._stage == "release", "Request object in wrong state" + assert a_resource.users[0] is request_object._simpy_event, "The user is the request object" + + new_request = ResourceHold(a_resource) + assert new_request._stage == "request", "Request object in wrong state" + new_request.as_event() + env.run() + + assert new_request._stage == "release", "Request object in wrong state" + assert new_request._simpy_event is not None + assert not new_request._simpy_event.processed, "Request went through when it shouldn't" + + # put the old one back + request_object.as_event() + env.run() + assert new_request._simpy_event is not None + assert new_request._simpy_event.processed, "Follow-on request didn't go through" + + newest_request = ResourceHold(a_resource) + assert newest_request._stage == "request", "Request object in wrong state" + newest_request.as_event() + env.run() + + # cancel it + assert newest_request._stage == "release", "Request object in wrong state" + assert newest_request._simpy_event is not None + assert not newest_request._simpy_event.processed, "Request went through when it shouldn't" + + assert newest_request._simpy_event in a_resource.put_queue, ( + "Resource isn't waiting to be gathered" + ) + with pytest.raises(SimulationError, match="Resource release requested.*?"): + newest_request.as_event() + + newest_request.cancel() + env.run() + assert newest_request._simpy_event not in a_resource.put_queue, ( + "Resource hasn't left the wait queue" + ) + + +def test_multi_event() -> None: + with EnvironmentContext() as env: + with pytest.warns(UserWarning): + event1 = Wait(1.0) + event3 = SIM.Timeout(env, 1.5) + All(event1, event3) # type: ignore [arg-type] + + with EnvironmentContext() as env: + w = Wait(1.0) + e = Event() + evt = All(w, e) + w.as_event() + e.as_event() + evt.as_event() + + with pytest.raises(SimulationError, match="failed to cancel"): + e.cancel() + evt.cancel() + + +def test_and_event() -> None: + with EnvironmentContext() as env: + + def run(env: SIM.Environment, data: dict[str, float]) -> SIMPY_GEN: + event1 = Wait(1.0) + event2 = Wait(1.5) + + event = All(event1, event2) + yield event.as_event() + data["time"] = env.now + + data: dict[str, float] = {} + env.process(run(env, data)) + env.run() + assert data["time"] == 1.5 + + +def test_or_event() -> None: + with EnvironmentContext() as env: + data = { + "time": 0.0, + } + + def run(env: SIM.Environment) -> SIMPY_GEN: + event1 = Wait(1.0) + event2 = Wait(1.5) + + event = Any(event1, event2) + yield event.as_event() + data["time"] = env.now + + env.process(run(env)) + env.run() + # SimPy still runs the simulation long enough to finish the timeout + assert data["time"] == 1.0 + + +def test_composite() -> None: + with EnvironmentContext() as env: + data = { + "time": 0.0, + "result": 0, + } + + def run(env: SIM.Environment) -> SIMPY_GEN: + event1 = Wait(1.0) + event2 = Wait(1.5) + + event3 = Wait(2.1) + event4 = Wait(0.9) + + event_a = Any(event1, event2) + event_b = All(event3, event4, event_a) + result = yield event_b.as_event() + data["time"] = env.now + data["result"] = len(result.events) + + env.process(run(env)) + env.run() + assert data["time"] == 2.1 + assert data["result"] == 4 + + +def run_one(env: SIM.Environment, event: Event) -> SIMPY_GEN: + yield env.timeout(1.0) + event.succeed(data="here") + + +def run_two(env: SIM.Environment, event: Event, data: dict[str, float]) -> SIMPY_GEN: + yield event._simpy_event + data["time_two"] = env.now + + +def run_three(env: SIM.Environment, event: Event, data: dict[str, float]) -> SIMPY_GEN: + yield event._simpy_event + data["time_three"] = env.now + + +def run_four(env: SIM.Environment, event: Event) -> SIMPY_GEN: + yield env.timeout(1.1) + event.succeed() + + +def run_four_alt(env: SIM.Environment, event: Event) -> SIMPY_GEN: + yield env.timeout(1.1) + event.succeed() + + +def run_five(env: SIM.Environment, event: Event, data: dict[str, float]) -> SIMPY_GEN: + # Timeout until after the event suceeded, but before its reset + yield env.timeout(1.05) + yield event.as_event() + data["time_five"] = env.now + + +def test_basic_usage() -> None: + with EnvironmentContext() as env: + event = Event() + assert event._simpy_event is not None + assert isinstance(event._simpy_event, SIM.Event) + assert event._simpy_event is event.as_event() + assert event.is_complete() is False + + env.process(run_one(env, event)) + data: dict[str, float] = {} + env.process(run_two(env, event, data)) + env.run() + assert data["time_two"] == 1.0 + assert event.is_complete() + payload = event.get_payload() + assert payload == {"data": "here"} + + with pytest.raises(SimulationError): + event.succeed() + + assert event.rehearsal_time_to_complete == 0.0 + + last_event = event._simpy_event + event.reset() + assert last_event is not event._simpy_event + +test_basic_usage() + +def test_conflicts() -> None: + data: dict[str, float] = {} + with EnvironmentContext() as env: + event = Event() + env.process(run_one(env, event)) + env.process(run_two(env, event, data)) + env.process(run_three(env, event, data)) + env.run() + assert data["time_two"] == data["time_three"] + + data: dict[str, float] = {} + with EnvironmentContext() as env: + with pytest.raises(SimulationError): + event = Event() + env.process(run_one(env, event)) + env.process(run_two(env, event, data)) + env.process(run_three(env, event, data)) + env.process(run_four(env, event)) + env.run() + + data: dict[str, float] = {} + with EnvironmentContext() as env: + event = Event() + env.process(run_one(env, event)) + env.process(run_two(env, event, data)) + env.process(run_three(env, event, data)) + env.process(run_four_alt(env, event)) + env.process(run_five(env, event, data)) + env.run() + assert data["time_two"] == data["time_three"] + assert data["time_five"] == 1.1 + + +def test_resubmit_wait_events() -> None: + # Test the bug found in github issue 41 + # Where Wait wasn't acting like timeout for being resubmitted. + + # This illustrates the simpy behavior + with EnvironmentContext() as env: + w1 = Wait(1.1) + w2 = Wait(2.2) + + # put the events into simpy + w1.as_event() + w2.as_event() + + env.run() + assert env.now == 2.2 + + # even when the timeout has no callbacks, it completes + with EnvironmentContext() as env: + w1 = Wait(1.1) + w2 = Wait(2.2) + + def _proc() -> SIMPY_GEN: + yield w1.as_event() | w2.as_event() + assert env.now == 1.1 + + env.process(_proc()) + env.run() + assert env.now == 2.2 + + # if we re-wait on w2, it should end at the right time. + with EnvironmentContext() as env: + w1 = Wait(1.1) + w2 = Wait(2.2) + + def _proc() -> SIMPY_GEN: + yield w1.as_event() | w2.as_event() + assert env.now == 1.1 + yield w2.as_event() + assert env.now == 2.2 + + env.process(_proc()) + env.run() + assert env.now == 2.2 + + +def test_resubmit_get_put_events() -> None: + # Make sure that get/put events don't hang. + with EnvironmentContext() as env: + store1 = SIM.Store(env) + store2 = SIM.Store(env) + + def _put_stuff() -> SIMPY_GEN: + yield Wait(1.0).as_event() + yield store1.put("thing") + yield Wait(1.0).as_event() + yield store2.put("other") + + def _get_stuff() -> SIMPY_GEN: + g1 = Get(store1) + g2 = Get(store2) + yield g1.as_event() | g2.as_event() + assert g1.is_complete() + assert g1.get_value() == "thing" + assert env.now == 1.0 + yield g2.as_event() + assert env.now == 2.0 + assert g2.is_complete() + assert g2.get_value() == "other" + + env.process(_put_stuff()) + env.process(_get_stuff()) + env.run() + + # run the same through UPSTAGE + with EnvironmentContext() as env: + store1 = SIM.Store(env) + store2 = SIM.Store(env) + + def _put_stuff() -> SIMPY_GEN: + yield Wait(1.0).as_event() + yield store1.put("thing") + yield Wait(1.0).as_event() + yield store2.put("other") + + def _get_stuff() -> SIMPY_GEN: + g1 = Get(store1) + g2 = Get(store2) + yield Any(g1, g2).as_event() + assert g1.is_complete() + assert g1.get_value() == "thing" + assert env.now == 1.0 + yield g2.as_event() + assert env.now == 2.0 + assert g2.is_complete() + assert g2.get_value() == "other" + + env.process(_put_stuff()) + env.process(_get_stuff()) + env.run() + + +def test_filter_store_events() -> None: + with EnvironmentContext() as env: + store1 = SIM.FilterStore(env) + + def _put_stuff() -> SIMPY_GEN: + yield Wait(1.0).as_event() + yield store1.put("thing") + yield Wait(1.0).as_event() + yield store1.put("other") + + def _get_stuff() -> SIMPY_GEN: + g1 = FilterGet(store1, filter=lambda item: "oth" in item) + yield g1.as_event() + assert g1.get_value() == "other" + assert "thing" in store1.items + + env.process(_put_stuff()) + env.process(_get_stuff()) + env.run() + + +test_filter_store_events() diff --git a/tests/test_knowledge.py b/tests/test_knowledge.py new file mode 100644 index 0000000..1a1cd46 --- /dev/null +++ b/tests/test_knowledge.py @@ -0,0 +1,58 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test knowledge.""" + +from typing import TypedDict + +import pytest +from upstage_des.actor import EMPTY_KNOWLEDGE, Actor +from upstage_des.base import EnvironmentContext, SimulationError + + +def test_knowledge() -> None: + class TD(TypedDict): + number: int + message: float + + class MyActor(Actor): + fuel: float = 120. + knowledge: TD + + class NoKnow(Actor):... + + with EnvironmentContext(): + ma = MyActor( + name="act", + knowledge={"number": 2, "message":3.0, "other":"string"} + ) + assert ma.knowledge["message"] == 3.0 + assert ma.knowledge["number"] == 2 + assert ma.knowledge["other"] == "string" + nk = NoKnow(name="no knowledge") + assert len(nk.knowledge) == 0 + nk.knowledge["new data"] = 2.3 + assert len(nk.knowledge) == 1 + + v = ma.get_knowledge("message") + assert v == 3.0 + v = ma.get_knowledge("number", must_exist=True) + assert v == 2 + with pytest.raises(SimulationError, match="does not exist on"): + ma.get_knowledge("foo", must_exist=True) + assert ma.get_knowledge("foo") is EMPTY_KNOWLEDGE + assert nk.get_knowledge("foo") is EMPTY_KNOWLEDGE + with pytest.raises(SimulationError, match="does not exist on"): + nk.get_knowledge("foo", must_exist=True) + + ma.clear_knowledge("number") + v = ma.get_knowledge("number") + assert v is EMPTY_KNOWLEDGE + ma.set_knowledge("number", 12.0, caller="The Test") + assert ma.get_knowledge("number", must_exist=True) == 12.0 + assert len(ma.get_log()) == 2 + + +test_knowledge() diff --git a/tests/test_stage.py b/tests/test_stage.py new file mode 100644 index 0000000..5e52a15 --- /dev/null +++ b/tests/test_stage.py @@ -0,0 +1,422 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test the stage.""" + +import pytest +from random import Random + +from upstage_des.base import ( + EnvironmentContext, + Stage, + UpstageBase, + UpstageError, + add_stage_variable, + get_stage, + get_stage_variable, +) + + +def test_stage_default_values() -> None: + with EnvironmentContext(): + stage = get_stage() + assert stage.altitude_units == "ft" + assert stage.distance_units == "nmi" + assert stage.time_unit == "hr" + assert stage.daily_time_count == 24.0 + assert stage.debug_log_time is False + assert isinstance(stage.userdata, dict) + assert len(stage.userdata) == 0 + + +def test_stage_custom_initialization() -> None: + rng = Random(42) + stage = Stage( + random=rng, + altitude_units="m", + distance_units="km", + time_unit="s", + daily_time_count=86400.0, + debug_log_time=True, + ) + with EnvironmentContext(stage=stage): + retrieved_stage = get_stage() + assert retrieved_stage is stage + assert retrieved_stage.altitude_units == "m" + assert retrieved_stage.distance_units == "km" + assert retrieved_stage.time_unit == "s" + assert retrieved_stage.daily_time_count == 86400.0 + assert retrieved_stage.debug_log_time is True + + +def test_stage_random_with_seed() -> None: + with EnvironmentContext(random_seed=12345): + stage = get_stage() + assert isinstance(stage.random, Random) + value1 = stage.random.random() + + with EnvironmentContext(random_seed=12345): + stage = get_stage() + value2 = stage.random.random() + + assert value1 == value2 + + +def test_stage_random_with_generator() -> None: + rng = Random(99999) + expected_value = rng.random() + + rng = Random(99999) + with EnvironmentContext(random_gen=rng): + stage = get_stage() + value = stage.random.random() + + assert value == expected_value + + +def test_stage_random_generator_takes_precedence() -> None: + rng = Random(99999) + with EnvironmentContext(random_seed=12345, random_gen=rng): + stage = get_stage() + assert stage.random is rng + + +def test_stage_access_from_upstage_base() -> None: + with EnvironmentContext(): + base = UpstageBase() + stage = base.stage + assert stage.altitude_units == "ft" + + +def test_stage_not_available_outside_context() -> None: + with pytest.raises(LookupError): + get_stage() + + +def test_stage_property_error_outside_context() -> None: + base = UpstageBase() + with pytest.raises(LookupError): + _ = base.stage + + +def test_stage_set_altitude_units_once() -> None: + with EnvironmentContext(): + stage = get_stage() + assert stage.altitude_units == "ft" + + stage.altitude_units = "miles" + assert stage.altitude_units == "miles" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.altitude_units = "meters" + + +def test_stage_set_distance_units_once() -> None: + with EnvironmentContext(): + stage = get_stage() + assert stage.distance_units == "nmi" + + stage.distance_units = "km" + assert stage.distance_units == "km" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.distance_units = "miles" + + +def test_stage_set_time_unit_once() -> None: + with EnvironmentContext(): + stage = get_stage() + assert stage.time_unit == "hr" + + stage.time_unit = "s" + assert stage.time_unit == "s" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.time_unit = "min" + + +def test_stage_set_daily_time_count_once() -> None: + with EnvironmentContext(): + stage = get_stage() + assert stage.daily_time_count == 24.0 + + stage.daily_time_count = 86400.0 + assert stage.daily_time_count == 86400.0 + + with pytest.raises(UpstageError, match="can only be set once"): + stage.daily_time_count = 1440.0 + + +def test_stage_set_debug_log_time_once() -> None: + with EnvironmentContext(): + stage = get_stage() + assert stage.debug_log_time is False + + stage.debug_log_time = True + assert stage.debug_log_time is True + + with pytest.raises(UpstageError, match="can only be set once"): + stage.debug_log_time = False + + +def test_stage_set_random_once() -> None: + with EnvironmentContext(): + stage = get_stage() + original_rng = stage.random + + new_rng = Random(42) + stage.random = new_rng + assert stage.random is new_rng + + with pytest.raises(UpstageError, match="can only be set once"): + stage.random = Random(100) + + +def test_stage_set_all_attributes_once() -> None: + rng = Random(42) + with EnvironmentContext(random_gen=rng): + stage = get_stage() + + stage.altitude_units = "m" + assert stage.altitude_units == "m" + + stage.distance_units = "km" + assert stage.distance_units == "km" + + stage.time_unit = "s" + assert stage.time_unit == "s" + + stage.daily_time_count = 86400.0 + assert stage.daily_time_count == 86400.0 + + stage.debug_log_time = True + assert stage.debug_log_time is True + + new_rng = Random(100) + stage.random = new_rng + assert stage.random is new_rng + + with pytest.raises(UpstageError, match="can only be set once"): + stage.altitude_units = "ft" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.distance_units = "nmi" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.time_unit = "hr" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.daily_time_count = 24.0 + + with pytest.raises(UpstageError, match="can only be set once"): + stage.debug_log_time = False + + with pytest.raises(UpstageError, match="can only be set once"): + stage.random = Random(200) + + +def test_add_stage_variable() -> None: + with EnvironmentContext(): + add_stage_variable("custom_var", 42) + stage = get_stage() + assert stage.userdata["custom_var"] == 42 + + +def test_get_stage_variable_from_userdata() -> None: + with EnvironmentContext(): + add_stage_variable("custom_var", "test_value") + value = get_stage_variable("custom_var") + assert value == "test_value" + + +def test_get_stage_variable_from_stage_attribute() -> None: + with EnvironmentContext(): + value = get_stage_variable("altitude_units") + assert value == "ft" + + +def test_add_stage_variable_duplicate_error() -> None: + with EnvironmentContext(): + add_stage_variable("my_var", 10) + with pytest.raises(UpstageError, match="already exists in the stage userdata"): + add_stage_variable("my_var", 20) + + +def test_add_stage_variable_conflicts_with_stage_attribute() -> None: + with EnvironmentContext(): + with pytest.raises(UpstageError, match="already exists in the stage"): + add_stage_variable("altitude_units", "m") + + +def test_get_stage_variable_nonexistent() -> None: + with EnvironmentContext(): + with pytest.raises(UpstageError, match="does not exist"): + get_stage_variable("nonexistent_var") + + +def test_stage_userdata_mutable() -> None: + with EnvironmentContext(): + stage = get_stage() + stage.userdata["test_key"] = "test_value" + assert stage.userdata["test_key"] == "test_value" + + stage.userdata["test_key"] = "updated_value" + assert stage.userdata["test_key"] == "updated_value" + + +def test_stage_attributes_independent_across_contexts() -> None: + with EnvironmentContext(): + stage1 = get_stage() + stage1.altitude_units = "m" + assert stage1.altitude_units == "m" + + with EnvironmentContext(): + stage2 = get_stage() + assert stage2.altitude_units == "ft" + stage2.altitude_units = "km" + assert stage2.altitude_units == "km" + + +def test_stage_shared_across_instances() -> None: + with EnvironmentContext(): + base1 = UpstageBase() + base2 = UpstageBase() + + stage1 = base1.stage + stage2 = base2.stage + + assert stage1 is stage2 + + +def test_stage_context_isolation() -> None: + with EnvironmentContext(random_seed=1): + stage1 = get_stage() + stage1.userdata["test"] = "value1" + + with EnvironmentContext(random_seed=2): + stage2 = get_stage() + assert "test" not in stage2.userdata + stage2.userdata["test"] = "value2" + assert stage2.userdata["test"] == "value2" + + assert stage1.userdata["test"] == "value1" + + +def test_stage_random_different_without_seed() -> None: + with EnvironmentContext(): + stage1 = get_stage() + value1 = stage1.random.random() + + with EnvironmentContext(): + stage2 = get_stage() + value2 = stage2.random.random() + + assert value1 != value2 + + +def test_multiple_random_calls_same_sequence() -> None: + with EnvironmentContext(random_seed=12345): + stage = get_stage() + values1 = [stage.random.random() for _ in range(5)] + + with EnvironmentContext(random_seed=12345): + stage = get_stage() + values2 = [stage.random.random() for _ in range(5)] + + assert values1 == values2 + + +def test_stage_userdata_complex_types() -> None: + with EnvironmentContext(): + stage = get_stage() + stage.userdata["dict_data"] = {"nested": {"key": "value"}} + stage.userdata["list_data"] = [[1, 2], [3, 4]] + stage.userdata["set_data"] = {1, 2, 3} + + assert stage.userdata["dict_data"]["nested"]["key"] == "value" + assert stage.userdata["list_data"][1][0] == 3 + assert 2 in stage.userdata["set_data"] + + +def test_stage_userdata_always_mutable() -> None: + with EnvironmentContext(): + stage = get_stage() + + stage.userdata["key1"] = "value1" + assert stage.userdata["key1"] == "value1" + + stage.userdata["key1"] = "value2" + assert stage.userdata["key1"] == "value2" + + stage.userdata["key1"] = "value3" + assert stage.userdata["key1"] == "value3" + + +def test_stage_read_before_set() -> None: + with EnvironmentContext(): + stage = get_stage() + + assert stage.altitude_units == "ft" + + stage.altitude_units = "m" + assert stage.altitude_units == "m" + + +def test_stage_multiple_attributes_set_independently() -> None: + with EnvironmentContext(): + stage = get_stage() + + stage.altitude_units = "m" + assert stage.altitude_units == "m" + + stage.distance_units = "km" + assert stage.distance_units == "km" + + with pytest.raises(UpstageError, match="can only be set once"): + stage.altitude_units = "ft" + + stage.time_unit = "s" + assert stage.time_unit == "s" + + +def test_stage_set_then_read_multiple_times() -> None: + with EnvironmentContext(): + stage = get_stage() + + stage.altitude_units = "yards" + assert stage.altitude_units == "yards" + assert stage.altitude_units == "yards" + assert stage.altitude_units == "yards" + + +def test_environment_context_with_stage_instance() -> None: + rng = Random(42) + stage = Stage( + random=rng, + altitude_units="meters", + distance_units="km", + time_unit="min", + ) + + with EnvironmentContext(stage=stage): + retrieved_stage = get_stage() + assert retrieved_stage is stage + assert retrieved_stage.altitude_units == "meters" + assert retrieved_stage.distance_units == "km" + assert retrieved_stage.time_unit == "min" + assert retrieved_stage.random is rng + + +def test_environment_context_with_stage_instance_ignores_random_seed() -> None: + rng = Random(100) + stage = Stage( + random=rng, + altitude_units="feet", + ) + + with EnvironmentContext(random_seed=999, stage=stage): + retrieved_stage = get_stage() + assert retrieved_stage is stage + assert retrieved_stage.random is rng diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 0000000..eac3784 --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,410 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test singular tasks.""" + +from inspect import isgeneratorfunction +from typing import Any, TypedDict, cast + +import pytest +from simpy import Environment, Process + +from upstage_des.actor import Actor +from upstage_des.base import SIMPY_GEN, EnvironmentContext, SimulationError +from upstage_des.events import Wait +from upstage_des.states import LinearChangingState, State +from upstage_des.tasks import InterruptStates, Task, TASK_GEN, TerminalTask + +class Know(TypedDict): + thing1: str + thing2: float + + +class TaskedActor(Actor): + time: float + knowledge: Know + + +class KnowTask(Task): + """A task for testing.""" + def task(self, *, actor: Actor) -> TASK_GEN: + self.set_actor_bulk_knowledge( + actor, + know = {"thing1": "Hello", "thing2": 6.28}, + ) + know = self.get_actor_bulk_knowledge( + actor, + ("thing1", "thing2"), + must_exist=True, + ) + assert know == {"thing1": "Hello", "thing2": 6.28} + yield Wait(actor.time) + + +class ActorForTest(Actor): + dummy: float = State(recording=True) + + +class ActorChangeForTest(Actor): + dummy: float = LinearChangingState() + + +class Dummy(Actor): + status: str + rate: float + changer: float = LinearChangingState(recording=True) + + +class WorkingTask(Task): + times: list[float] + + def task(self, *, actor: ActorForTest) -> TASK_GEN: + for wait_period in self.times: + the_event = Wait(wait_period) + yield the_event + actor.dummy += wait_period + + +class ChangingTask(Task): + times: list[float] + rate: float + + def task(self, *, actor: ActorForTest) -> TASK_GEN: + for t in self.times: + the_event = Wait(t) + actor.activate_state(state="dummy", cause=self, rate=self.rate) + yield the_event + actor.deactivate_state(state="dummy", cause=self) + + +class Actor2Test(Actor): + dummy: Any = State() + + +class WorkingTask2(Task): + times: list[float] + log: list[str] + + def task(self, *, actor: ActorChangeForTest) -> TASK_GEN: + for wait_period in self.times: + wait_event = Wait(wait_period) + self.log.append( + f"{self.env.now}: {self.__class__.__name__} " + f"waiting {wait_period}, value={actor.dummy}" + ) + yield wait_event + self.log.append( + f"{self.env.now}: {self.__class__.__name__} " + f"finished waiting {wait_period}, " + f"value={actor.dummy}" + ) + actor.dummy += wait_period + + +class ChangingTask2(Task): + times: list[float] + rate: float + log: list[str] + + def task(self, *, actor: ActorForTest | ActorChangeForTest) -> TASK_GEN: + for wait_period in self.times: + wait_event = Wait(wait_period) + actor.activate_state(state="dummy", cause=self, rate=self.rate) + actor.set_knowledge("example for logging", "a value", overwrite=True) + self.log.append( + f"{self.env.now}: {self.__class__.__name__} " + f"waiting {wait_period}, value={actor.dummy}" + ) + yield wait_event + self.log.append( + f"{self.env.now}: {self.__class__.__name__} finished " + f"waiting {wait_period}, value={actor.dummy}" + ) + actor.deactivate_state(state="dummy", cause=self) + + +def _task_runner(env: Environment, rate: float, timeout_point: float, final: bool=False) -> SIMPY_GEN: + use_actor = ActorChangeForTest(name="testing", dummy=0.0, debug_log=True) + times = [1.0, 2.0] + + task_object = ChangingTask2() + task_object.times = times + task_object.rate = rate + task_object.log = [] + + task_generator = task_object.run( + actor=use_actor, + ) + timeout = env.timeout(timeout_point) + + yield task_generator | timeout + + if task_generator.is_alive: + task_object._final_interrupt = final + task_generator.interrupt("cancelling") + + return use_actor + + +def test_task() -> None: + with EnvironmentContext() as env: + a = TaskedActor( + name="example", + time=3.0, + ) + t = KnowTask() + t.run(actor=a) + env.run() + assert env.now == a.time + thelog = a.get_log() + assert len(thelog) == 2 + assert "Reason: KnowTask" in thelog[0][1] + assert "Reason: KnowTask" in thelog[1][1] + + +def test_failures_for_tasks_with_simpy_events() -> None: + with EnvironmentContext() as env: + actor = ActorForTest(name="testing", dummy=0.) + + class BrokenTask(Task): + def task(self, *, actor: ActorForTest) -> TASK_GEN: + yield self.env.timeout(1.0) # type: ignore [misc, union-attr] + + # msg = "*Task is yielding objects without `as_event`*" + with pytest.raises(SimulationError): # , match=msg): + the_task = BrokenTask() + _ = the_task.run( + actor=actor, + ) + env.run() + + # msg = "*'MockEnvironment' object has no attribute 'timeout'*" + with pytest.raises(AttributeError): # , match=msg): + the_task = BrokenTask() + the_task.rehearse( + actor=actor, + ) + + +def test_failures_for_tasks_with_incorrect_events() -> None: + with EnvironmentContext(): + actor = ActorForTest(name="testing", dummy=0.) + + class BlankEvent: + def __init__(self, **kwargs: Any) -> None: + pass + + class BrokenTask(Task): + def __init__(self, t: Any) -> None: + super().__init__() + self.event_class = t + + def task(self, *, actor: ActorForTest) -> TASK_GEN: + yield self.event_class() + + # msg = '*must be a subclass of BaseEvent*' + with EnvironmentContext() as env: + with pytest.raises(SimulationError): + task_instance = BrokenTask(BlankEvent) + task_instance.run(actor=actor) + env.run() + + +def test_running() -> None: + with EnvironmentContext() as env: + actor = ActorForTest(name="testing", dummy=0.) + times = [1.0, 2.0] + + task_object = WorkingTask() + task_object.times = times + task_process = task_object.run( + actor=actor, + ) + env.run() + assert env.now == 3, "Environment time must increase" + assert actor.dummy == 3, "Actor state must change" + assert isinstance(task_process, Process), "Task process is not an instance of simpy.Process" + + +def test_interrupting() -> None: + with EnvironmentContext() as env: + timeout_point = 0.5 + rate = 0.5 + proc = env.process( + _task_runner( + env, + rate=rate, + timeout_point=timeout_point, + ) + ) + env.run() + actor = cast(ActorForTest, proc.value) + msg = "Task interruption ended at the wrong time" + assert actor.dummy == timeout_point * rate, msg + + +def test_interrupting_two() -> None: + # Do the timeout right when a time will end + with EnvironmentContext() as env: + timeout_point = 1.0 + rate = 3.5 + proc = env.process( + _task_runner( + env=env, + rate=rate, + timeout_point=timeout_point, + ) + ) + env.run() + actor = cast(ActorForTest, proc.value) + msg = "Task interruption ended at the wrong time" + assert actor.dummy == timeout_point * rate, msg + + +def test_interrupting_final() -> None: + with EnvironmentContext() as env: + timeout_point = 0.5 + rate = 3.5 + proc = env.process( + _task_runner( + env=env, + rate=rate, + timeout_point=timeout_point, + final=True, + ) + ) + env.run() + actor = cast(ActorForTest, proc.value) + msg = "Task interruption ended at the wrong time" + assert actor.dummy == timeout_point * rate, msg + + +def test_simultaneous_task() -> None: + with EnvironmentContext() as env: + actor = ActorChangeForTest(name="testing", dummy=0.0) + + def task_runner( + *, + task_class: type[WorkingTask2 | ChangingTask2], + interrupt_time: float, + **task_kwargs: Any, + ) -> SIMPY_GEN: + task = task_class() + task.log = [] + for k, v in task_kwargs.items(): + setattr(task, k, v) + running_task = task.run( + actor=actor, + ) + + timeout = env.timeout(interrupt_time) + + yield running_task | timeout + + if running_task.is_alive: + running_task.interrupt("cancelling") + + return actor + + _ = env.process( + task_runner( + task_class=ChangingTask2, + rate=1.0, + times=[1.0, 3.0, 5.0, 6.0], + interrupt_time=10.0, + ) + ) + + _ = env.process( + task_runner( + task_class=WorkingTask2, + times=[2.0, 2.0, 2.0, 10.0], + interrupt_time=12.0, + ) + ) + + env.run(until=20.0) + + +class Restartable(Task): + def task(self, *, actor: Dummy) -> TASK_GEN: + actor.activate_state( + state="changer", + cause=self, + rate=actor.rate, + ) + self.set_marker("change to test") + yield Wait(10.0) + actor.deactivate_all_states(cause=self) + + def on_interrupt(self, *, actor: Dummy, cause: Any) -> InterruptStates: + if cause == "restart": + return InterruptStates.RESTART + else: + return InterruptStates.END + + +def test_restart() -> None: + with EnvironmentContext() as env: + actor = Dummy( + name="Example", + status="available", + rate=2.3, + changer=0.0, + debug_log=True, + ) + + task = Restartable() + proc = task.run(actor=actor) + env.run(until=3.4) + proc.interrupt(cause="restart") + env.run() + assert pytest.approx(actor.changer) == 2.3 * (3.4 + 10) + + +def test_terminal_task_run( +) -> None: + class EndPoint(TerminalTask): + def log_message(self, *, actor: Actor) -> str: + return "The Message" + + class EndPointBase(TerminalTask): + pass + + class Dummy(Actor): + status: Any = State() + + with EnvironmentContext() as env: + actor = Dummy(name="x", status="Good", debug_log=True) + task = EndPoint() + + proc = task.run(actor=actor) + env.run() + assert env.now == 0 + + assert "The Message" in actor._log[-1][1] + + with pytest.raises(SimulationError, match=".+Cannot interrupt a terminal.+"): + proc.interrupt() + env.run() + + actor = Dummy(name="x", status="Good", debug_log=True) + task = EndPointBase() + proc = task.run(actor=actor) + env.run() + assert env.now == 0 + + assert "Entering terminal task:" in actor._log[-1][1] + + # See if the final interrupt value keeps it from failing. + with EnvironmentContext() as env: + actor = Dummy(name="x", status="Good", debug_log=True) + task = EndPoint() + + proc = task.run(actor=actor) + env.run() + task._final_interrupt = True + proc.interrupt(cause="FINAL") + env.run() diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..b02332b --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,32 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test unit conversion.""" + +from itertools import combinations + +import pytest + +from upstage_des.units import _distance_to_m, _time_to_s, unit_convert, speed_convert + + +def test_convert_fail() -> None: + with pytest.raises(ValueError): + unit_convert(100, "parsec", "km") + + +def test_convert_reverse() -> None: + for unit_grp in [_distance_to_m, _time_to_s]: + for unit_1, unit_2 in combinations(unit_grp, 2): + ans = unit_convert(1.0, unit_1, unit_2) + reverse = unit_convert(ans, unit_2, unit_1) + assert pytest.approx(reverse) == 1 + + +def test_convert_sped() -> None: + kps = speed_convert(60, "miles", "hour", "km", "second") + assert kps == pytest.approx(0.0268224) + kts = speed_convert(1, "meters", "second", "nmi", "hr") + assert kts == pytest.approx(1.94384, rel=0.001) From 6bd414a7f4b04d714e7078f75ca2676d6c8825d1 Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 17:28:18 -0400 Subject: [PATCH 03/12] Multi-env fixes. --- pixi.lock | 2641 ++++++++++++++++++++++++++++++++++++- pixi.toml | 14 + src/upstage_des/actor.py | 1 - src/upstage_des/states.py | 4 +- src/upstage_des/tasks.py | 2 +- 5 files changed, 2653 insertions(+), 9 deletions(-) diff --git a/pixi.lock b/pixi.lock index c19d5b2..920a461 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1978,6 +1978,381 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313ha7868ed_1.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ + py314: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py314h67df5f8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py314hae3bed6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py314h5bd0f2a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.21.1-py310hd8a072f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py314h0f05182_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.11-h7805a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl + - pypi: ./ + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py314h3262eb8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py314h77fa6c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py314h1d4708b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py314h77fa6c7_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py314h217eccc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py314hd330473_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.21.1-py310hb9b2626_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.4-h7c6738f_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py314h0b69929_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py314h10d0514_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.11-h16586dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl + - pypi: ./ + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py314h6e9b3f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py314h264e108_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py314hbdd0d06_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.21.1-py310h3b8a9b8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py314ha14b1ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.11-hc5c3a1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl + - pypi: ./ + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py314h2359020_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py314hcdb55d9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.21.1-py310ha413424_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.11-h02f8532_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl + - pypi: ./ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -1986,6 +2361,20 @@ packages: purls: [] size: 2562 timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 build_number: 16 sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 @@ -2011,6 +2400,17 @@ packages: purls: [] size: 8283 timestamp: 1736938720099 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + purls: [] + size: 8191 + timestamp: 1744137672556 - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda sha256: 1307719f0d8ee694fc923579a39c0621c23fdaa14ccdf9278a5aac5665ac58e9 md5: 74ac5069774cdbc53910ec4d631a3999 @@ -2232,6 +2632,30 @@ packages: - pkg:pypi/babel?source=compressed-mapping size: 6938256 timestamp: 1738490268466 +- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 + md5: f1976ce927373500cc19d3c0b2c85177 + depends: + - python >=3.10 + - python + constrains: + - pytz >=2015.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/babel?source=compressed-mapping + size: 7684321 + timestamp: 1772555330347 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + noarch: generic + sha256: de1755a35258eb1b59f2288559bbf0b76da60bd2fa6cd6f768ead442f85bd666 + md5: b712198b257f378e9bd8cde277218296 + depends: + - python >=3.14 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: [] + size: 7546 + timestamp: 1777848733980 - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda sha256: 4ce42860292a57867cfc81a5d261fb9886fc709a34eca52164cc8bbf6d03de9f md5: 373374a3ed20141090504031dc7b693e @@ -2245,6 +2669,19 @@ packages: - pkg:pypi/beautifulsoup4?source=compressed-mapping size: 145482 timestamp: 1738740460562 +- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 + md5: 5267bef8efea4127aacd1f4e1f149b6e + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=hash-mapping + size: 90399 + timestamp: 1764520638652 - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda sha256: a05971bb80cca50ce9977aad3f7fc053e54ea7d5321523efc7b9a6e12901d3cd md5: f0b4c8e370446ef89797608d60a564b3 @@ -2303,6 +2740,23 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 350424 timestamp: 1725267803672 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda + sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 + md5: 8910d2c46f7e7b519129f486e0fe927a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 367376 + timestamp: 1764017265553 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312h5861a67_2.conda sha256: 265764ff4ad9e5cfefe7ea85c53d95157bf16ac2c0e5f190c528e4c9c0c1e2d0 md5: b95025822e43128835826ec0cc45a551 @@ -2335,6 +2789,22 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 363156 timestamp: 1725268004102 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py314h3262eb8_1.conda + sha256: 2e34922abda4ac5726c547887161327b97c3bbd39f1204a5db162526b8b04300 + md5: 389d75a294091e0d7fa5a6fc683c4d50 + depends: + - __osx >=10.13 + - libcxx >=19 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 390153 + timestamp: 1764017784596 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hde4cb15_2.conda sha256: 254b411fa78ccc226f42daf606772972466f93e9bc6895eabb4cfda22f5178af md5: a83c2ef76ccb11bc2349f4f17696b15d @@ -2369,6 +2839,23 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 339067 timestamp: 1725268603536 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + sha256: 5c2e471fd262fcc3c5a9d5ea4dae5917b885e0e9b02763dbd0f0d9635ed4cb99 + md5: f9501812fe7c66b6548c7fcaa1c1f252 + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 359854 + timestamp: 1764018178608 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_2.conda sha256: f83baa6f6bcba7b73f6921d5c1aa95ffc5d8b246ade933ade79250de0a4c9c4c md5: a99aec1ac46794a5fb1cd3cf5d2b6110 @@ -2403,6 +2890,23 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 322309 timestamp: 1725268431915 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + sha256: 6854ee7675135c57c73a04849c29cbebc2fb6a3a3bfee1f308e64bf23074719b + md5: 1302b74b93c44791403cbeee6a0f62a3 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libbrotlicommon 1.2.0 hfd05255_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 335782 + timestamp: 1764018443683 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d md5: 62ee74e96c5ebb0af99386de58cf9553 @@ -2414,6 +2918,27 @@ packages: purls: [] size: 252783 timestamp: 1720974456583 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + sha256: 9f242f13537ef1ce195f93f0cc162965d6cc79da578568d6d8e50f70dd025c42 + md5: 4173ac3b19ec0a4f400b4f782910368b + depends: + - __osx >=10.13 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 133427 + timestamp: 1771350680709 - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda sha256: cad153608b81fb24fc8c509357daa9ae4e49dfc535b2cb49b91e23dbd68fc3c5 md5: 7ed4301d437b59045be7e051a0308211 @@ -2434,6 +2959,28 @@ packages: purls: [] size: 122909 timestamp: 1720974522888 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 + md5: 4cb8e6b48f67de0b018719cdf1136306 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 56115 + timestamp: 1771350256444 - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda sha256: 35a5dad92e88fdd7fc405e864ec239486f4f31eec229e31686e61a140a8e573b md5: 276e7ffe9ffe39688abc665ef0f45596 @@ -2453,6 +3000,24 @@ packages: purls: [] size: 158144 timestamp: 1738298224464 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + sha256: 6f4ff81534c19e76acf52fcabf4a258088a932b8f1ac56e9a59e98f6051f8e46 + md5: 56fb2c6c73efc627b40c77d14caecfba + depends: + - __win + license: ISC + purls: [] + size: 131388 + timestamp: 1776865633471 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d + md5: e18ad67cf881dcadee8b8d9e2f8e5f73 + depends: + - __unix + license: ISC + purls: [] + size: 131039 + timestamp: 1776865545798 - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda sha256: 42e911ee2d8808eacedbec46d99b03200a6138b8e8a120bd8acabe1cac41c63b md5: 3418b6c8cac3e71c0bc089fc5ea53042 @@ -2506,6 +3071,16 @@ packages: - pkg:pypi/certifi?source=compressed-mapping size: 162721 timestamp: 1739515973129 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878 + md5: 929471569c93acefb30282a22060dcd5 + depends: + - python >=3.10 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 135656 + timestamp: 1776866680878 - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda sha256: cba6ea83c4b0b4f5b5dc59cb19830519b28f95d7ebef7c9c5cf1c14843621457 md5: a861504bbea4161a9170b85d4d2be840 @@ -2643,6 +3218,17 @@ packages: - pkg:pypi/charset-normalizer?source=hash-mapping size: 47438 timestamp: 1735929811779 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 + md5: a9167b9571f3baa9d448faa2139d1089 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=compressed-mapping + size: 58872 + timestamp: 1775127203018 - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda sha256: 21ecead7268241007bf65691610cd7314da68c1f88113092af690203b5780db5 md5: 364ba6c9fb03886ac979b482f39ebb92 @@ -2677,6 +3263,21 @@ packages: - pkg:pypi/comm?source=hash-mapping size: 12103 timestamp: 1733503053903 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py314h67df5f8_0.conda + sha256: cf5f98a291c3a5489cb299bae38711d5dc21b88a00df981f3b1528781e18c909 + md5: 78f547b78ace7541c4f54c4268ac9d2e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 411308 + timestamp: 1773761119353 - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py312h178313f_0.conda sha256: 5c502e6a72f46af9e6dd74e9d91449898c72ccb36dad46db7a09101042eef0c2 md5: c9c5941aa3ad8c8324edf65128121395 @@ -2707,6 +3308,20 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 378570 timestamp: 1742591809856 +- conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py314h77fa6c7_0.conda + sha256: fc472d8fcc0e831faf1af1ed78f4db9bd62f311187f05d1c140626a9317b8a4e + md5: c5d3ea7d5f490c69a6af7c056624fca4 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 412623 + timestamp: 1773761406443 - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py312h3520af0_0.conda sha256: 0153ea1d55d1c4a88a9a7e6ba24332cdac3379c2e62874d7af91e8586606ede0 md5: 3aac48d735f438a582078c623c1f6a70 @@ -2735,6 +3350,21 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 377981 timestamp: 1742591939877 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py314h6e9b3f0_0.conda + sha256: 808ebcb57027251f379f84e53a3755d2851918f78bdd512d131afe40ca64a041 + md5: cdbafe4a3e605024e7372c9580f9d734 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 412458 + timestamp: 1773761280047 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py312h998013c_0.conda sha256: 9da0faf9092eea8ae195b0316c039a1e59a9e10c43d4a16660b70341515b15bb md5: 07426bb994e08ea760003047e7e6f68f @@ -2765,6 +3395,22 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 378772 timestamp: 1742591852148 +- conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py314h2359020_0.conda + sha256: 80a6a7be7eef784b8314a4cb563563c654e2180a0b2b31b232f79b2e7334aaf2 + md5: 849f0bd5b83d4fd59b41202b21bb3ca2 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - tomli + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 438927 + timestamp: 1773760993379 - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py312h31fea79_0.conda sha256: 135bd1283053bb0f83f5166d17a13cc9c73c6d8af2d3e09f57d360a81413f0af md5: b2d00351ff17283c886c65f1121c21a4 @@ -2819,6 +3465,17 @@ packages: purls: [] size: 47792 timestamp: 1739800762370 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + noarch: generic + sha256: 40dc224f2b718e5f034efd2332bc315a719063235f63673468d26a24770094ee + md5: f111d4cfaf1fe9496f386bc98ae94452 + depends: + - python >=3.14,<3.15.0a0 + - python_abi * *_cp314 + license: Python-2.0 + purls: [] + size: 49809 + timestamp: 1775614256655 - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda sha256: 8f0b338687f79ea87324f067bedddd2168f07b8eec234f0fe63b522344c6a919 md5: 089cf3a3becf0e2f403feaf16e921678 @@ -2940,6 +3597,16 @@ packages: - pkg:pypi/docutils?source=hash-mapping size: 402700 timestamp: 1733217860944 +- conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + sha256: 0d605569a77350fb681f9ed8d357cc71649b59a304099dc9d09fbeec5e84a65e + md5: d6bd3cd217e62bbd7efe67ff224cd667 + depends: + - python >=3.10 + license: CC-PDDC AND BSD-3-Clause AND BSD-2-Clause AND ZPL-2.1 + purls: + - pkg:pypi/docutils?source=hash-mapping + size: 438002 + timestamp: 1766092633160 - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda sha256: 8925dc378a2d5533905b478c69fd7ea7c72c664aa4b37075a2711079bc9222e3 md5: 18d4243b3d30352f9dea8e522f6ff4d1 @@ -2963,6 +3630,17 @@ packages: - pkg:pypi/exceptiongroup?source=hash-mapping size: 20486 timestamp: 1733208916977 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda sha256: 9abc6c128cd40733e9b24284d0462e084d4aff6afe614f0754aa8533ebe505e4 md5: a71efeae2c160f6789900ba2631a2c90 @@ -2974,6 +3652,17 @@ packages: - pkg:pypi/execnet?source=hash-mapping size: 38835 timestamp: 1733231086305 +- conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + sha256: 1acc6a420efc5b64c384c1f35f49129966f8a12c93b4bb2bdc30079e5dc9d8a8 + md5: a57b4be42619213a94f31d2c69c5dda7 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/execnet?source=hash-mapping + size: 39499 + timestamp: 1762974150770 - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda sha256: 7510dd93b9848c6257c43fdf9ad22adf62e7aa6da5f12a6a757aed83bcfedf05 md5: 81d30c08f9a3e556e8ca9e124b044d14 @@ -3022,6 +3711,20 @@ packages: - pkg:pypi/h2?source=hash-mapping size: 53888 timestamp: 1738578623567 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 95967 + timestamp: 1756364871835 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba md5: 0a802cb9888dd14eeefc611f05c40b6e @@ -3076,6 +3779,18 @@ packages: - pkg:pypi/hyperframe?source=hash-mapping size: 17397 timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12723451 + timestamp: 1773822285671 - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda sha256: 2e64307532f482a0929412976c8450c719d558ba20c0962832132fd0d07ba7a7 md5: d68d48a3060eb5abdc1cdc8e2a3a5966 @@ -3086,6 +3801,16 @@ packages: purls: [] size: 11761697 timestamp: 1720853679409 +- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + sha256: 1294117122d55246bb83ad5b589e2a031aacdf2d0b1f99fd338aa4394f881735 + md5: 627eca44e62e2b665eeec57a984a7f00 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12273764 + timestamp: 1773822733780 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 md5: 5eb22c1d7b3fc4abb50d92d621583137 @@ -3107,6 +3832,18 @@ packages: - pkg:pypi/idna?source=hash-mapping size: 49765 timestamp: 1733211921194 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48 + md5: fb7130c190f9b4ec91219840a05ba3ac + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=compressed-mapping + size: 59038 + timestamp: 1776947141407 - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 sha256: c2bfd7043e0c4c12d8b5593de666c1e81d67b83c474a0a79282cc5c4ef845460 md5: 7de5386c8fea29e76b303f37dde4c352 @@ -3118,6 +3855,17 @@ packages: - pkg:pypi/imagesize?source=hash-mapping size: 10164 timestamp: 1656939625410 +- conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + sha256: 5a047f9eac290e679b4e6f6f4cbfcc5acdfbf031a4f06824d4ddb590cdbb850b + md5: 92617c2ba2847cca7a6ed813b6f4ab79 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/imagesize?source=hash-mapping + size: 15729 + timestamp: 1773752188889 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745 md5: 63ccfdc3a3ce25b027b8767eb722fca8 @@ -3156,7 +3904,18 @@ packages: - pkg:pypi/iniconfig?source=hash-mapping size: 11474 timestamp: 1733223232820 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=hash-mapping + size: 13387 + timestamp: 1760831448842 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda sha256: 33cfd339bb4efac56edf93474b37ddc049e08b1b4930cf036c893cc1f5a1f32a md5: b40131ab6a36ac2c09b7c57d4d3fbf99 depends: @@ -3314,6 +4073,19 @@ packages: - pkg:pypi/jedi?source=hash-mapping size: 843646 timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=hash-mapping + size: 120685 + timestamp: 1764517220861 - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af md5: 446bd6c8cb26050d528881df495ce646 @@ -3749,6 +4521,19 @@ packages: purls: [] size: 671240 timestamp: 1740155456116 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 728002 + timestamp: 1774197446916 - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-20.1.1-hf95d169_0.conda sha256: b30ef239517cfffb71d8ece7b903afe2a1bac0425f5bd38976b35d3cbf77312b md5: 85cff0ed95d940c4762d5a99a6fe34ae @@ -3759,6 +4544,16 @@ packages: purls: [] size: 562132 timestamp: 1742449741333 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda + sha256: 596a0bdd5321c5e41a4734f18b35bcbc5d116079d13bc40d765fd93c32b285d1 + md5: 4394b1ba4b9604ac4e1c5bdc74451279 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 567125 + timestamp: 1776815441323 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.1-ha82da77_0.conda sha256: 80dd8ae3fbcf508ed72f074ada2c7784298e822e8d19c3b84c266bb31456d77c md5: 833c4899914bf96caf64b52ef415e319 @@ -3769,6 +4564,16 @@ packages: purls: [] size: 561543 timestamp: 1742449846779 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda + sha256: 25a0d02148a39b665d9c2957676faf62a4d2a58494d53b201151199a197db4b0 + md5: 448a1af83a9205655ee1cf48d3875ca3 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 569927 + timestamp: 1776816293111 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 md5: c277e0a4d549b03ac1e9d6cbbe3d017b @@ -3819,6 +4624,19 @@ packages: purls: [] size: 73304 timestamp: 1730967041968 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12 + md5: a3b390520c563d78cc58974de95a03e5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 77241 + timestamp: 1777846112704 - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda sha256: d10f43d0c5df6c8cf55259bce0fe14d2377eed625956cddce06f58827d288c59 md5: 20307f4049a735a78a29073be1be2626 @@ -3831,6 +4649,18 @@ packages: purls: [] size: 70758 timestamp: 1730967204736 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + sha256: 5ebcc413d0a75da926a8b9b681d7d12c9562993991ba49c90a9881c4a59bdc11 + md5: d2e01f78c1daaeb4d2aa870125ebcd7e + depends: + - __osx >=11.0 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 75242 + timestamp: 1777846416221 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 md5: 38d2656dd914feb0cab8c629370768bf @@ -3843,6 +4673,18 @@ packages: purls: [] size: 64693 timestamp: 1730967175868 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + sha256: f4b1cafc59afaede8fa0a2d9cf376840f1c553001acd72f6ead18bbc8ac8c49c + md5: 65466e82c09e888ca7560c11a97d5450 + depends: + - __osx >=11.0 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 68789 + timestamp: 1777846180142 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda sha256: 0c0447bf20d1013d5603499de93a16b6faa92d7ead870d96305c0f065b6a5a12 md5: eb383771c680aa792feb529eaf9df82f @@ -3857,6 +4699,20 @@ packages: purls: [] size: 139068 timestamp: 1730967442102 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 + md5: 264e350e035092b5135a2147c238aec4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.8.0.* + license: MIT + license_family: MIT + purls: [] + size: 71094 + timestamp: 1777846223617 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda sha256: 67a6c95e33ebc763c1adc3455b9a9ecde901850eb2fceb8e646cc05ef3a663da md5: e3eb7806380bc8bcecba6d749ad5f026 @@ -3868,6 +4724,17 @@ packages: purls: [] size: 53415 timestamp: 1739260413716 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda sha256: 7805fdc536a3da7fb63dc48e040105cd4260c69a1d2bf5804dadd31bde8bab51 md5: b8667b0d0400b8dcb6844d8e06b2027d @@ -3878,6 +4745,16 @@ packages: purls: [] size: 47258 timestamp: 1739260651925 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + sha256: 951958d1792238006fdc6fce7f71f1b559534743b26cc1333497d46e5903a2d6 + md5: 66a0dc7464927d0853b590b6f53ba3ea + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + purls: [] + size: 53583 + timestamp: 1769456300951 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca md5: 086914b672be056eb70fd4285b6783b6 @@ -3886,6 +4763,16 @@ packages: purls: [] size: 39020 timestamp: 1636488587153 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda sha256: 77922d8dd2faf88ac6accaeebf06409d1820486fde710cff6b554d12273e46be md5: 31d5107f75b2f204937728417e2e39e5 @@ -3898,6 +4785,18 @@ packages: purls: [] size: 40830 timestamp: 1739260917585 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 md5: ef504d1acbd74b7cc6849ef8af47dd03 @@ -3912,6 +4811,20 @@ packages: purls: [] size: 847885 timestamp: 1740240653082 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 + md5: 0aa00f03f9e39fb9876085dee11a85d4 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041788 + timestamp: 1771378212382 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 md5: a2222a6ada71fb478682efe483ce0f92 @@ -3932,6 +4845,26 @@ packages: purls: [] size: 459862 timestamp: 1740240588123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 + md5: 239c5e9546c38a1e884d69effcf4c882 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603262 + timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-only + purls: [] + size: 790176 + timestamp: 1754908768807 - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda sha256: 18a4afe14f731bfb9cf388659994263904d20111e42f841e9eea1bb6f91f4ab4 md5: e796ff8ddc598affdf7c173d6145f087 @@ -3951,6 +4884,24 @@ packages: purls: [] size: 669052 timestamp: 1740128415026 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + sha256: a1c8cecdf9966921e13f0ae921309a1f415dfbd2b791f2117cf7e8f5e61a48b6 + md5: 210a85a1119f97ea7887188d176db135 + depends: + - __osx >=10.13 + license: LGPL-2.1-only + purls: [] + size: 737846 + timestamp: 1754908900138 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + sha256: de0336e800b2af9a40bdd694b03870ac4a848161b35c8a2325704f123f185f03 + md5: 4d5a7445f0b25b6a3ddbb56e790f5251 + depends: + - __osx >=11.0 + license: LGPL-2.1-only + purls: [] + size: 750379 + timestamp: 1754909073836 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda sha256: d30780d24bf3a30b4f116fca74dedb4199b34d500fe6c52cced5f8cc1e926f03 md5: 450e6bdc0c7d986acf7b8443dce87111 @@ -3971,6 +4922,17 @@ packages: purls: [] size: 638142 timestamp: 1740128665984 +- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 + md5: 64571d1dd6cdcfa25d0664a5950fdaa2 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-only + purls: [] + size: 696926 + timestamp: 1754909290005 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f md5: 42d5b6a0f30d3c10cd88cb8584fda1cb @@ -3981,6 +4943,18 @@ packages: purls: [] size: 111357 timestamp: 1738525339684 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 113478 + timestamp: 1775825492909 - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda sha256: a895b5b16468a6ed436f022d72ee52a657f9b58214b91fabfab6230e3592a6dd md5: db9d7b0152613f097cdb61ccf9f70ef5 @@ -3990,6 +4964,17 @@ packages: purls: [] size: 103749 timestamp: 1738525448522 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + sha256: d9e2006051529aec5578c6efeb13bb6a7200a014b2d5a77a579e83a8049d5f3c + md5: becdfbfe7049fa248e52aa37a9df09e2 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 105724 + timestamp: 1775826029494 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda sha256: 560c59d3834cc652a84fb45531bd335ad06e271b34ebc216e380a89798fe8e2c md5: e3fd1f8320a100f2b210e690a57cd615 @@ -3999,6 +4984,17 @@ packages: purls: [] size: 98945 timestamp: 1738525462560 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e + md5: b1fd823b5ae54fbec272cea0811bd8a9 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 92472 + timestamp: 1775825802659 - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda sha256: 3f552b0bdefdd1459ffc827ea3bf70a6a6920c7879d22b6bfd0d73015b55227b md5: c48f6ad0ef0a555b27b233dfcab46a90 @@ -4010,6 +5006,19 @@ packages: purls: [] size: 104465 timestamp: 1738525557254 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 + md5: 8f83619ab1588b98dd99c90b0bfc5c6d + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 106486 + timestamp: 1775825663227 - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda sha256: d02d1d3304ecaf5c728e515eb7416517a0b118200cd5eacbe829c432d1664070 md5: aeb98fdeb2e8f25d43ef71fbacbeec80 @@ -4021,6 +5030,27 @@ packages: purls: [] size: 89991 timestamp: 1723817448345 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + sha256: 1096c740109386607938ab9f09a7e9bca06d86770a284777586d6c378b8fb3fd + md5: ec88ba8a245855935b871a7324373105 + depends: + - __osx >=10.13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 79899 + timestamp: 1769482558610 - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hfdf4475_0.conda sha256: 791be3d30d8e37ec49bcc23eb8f1e1415d911a7c023fa93685f2ea485179e258 md5: ed625b2e59dff82859c23dd24774156b @@ -4031,6 +5061,16 @@ packages: purls: [] size: 76561 timestamp: 1723817691512 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 + md5: 57c4be259f5e0b99a5983799a228ae55 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 73690 + timestamp: 1769482560514 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda sha256: f7917de9117d3a5fe12a39e185c7ce424f8d5010a6f97b4333e8a1dcb2889d16 md5: 7476305c35dd9acef48da8f754eedb40 @@ -4053,6 +5093,18 @@ packages: purls: [] size: 88657 timestamp: 1723861474602 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + sha256: 40dcd0b9522a6e0af72a9db0ced619176e7cfdb114855c7a64f278e73f8a7514 + md5: e4a9fc2bba3b022dad998c78856afe47 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 89411 + timestamp: 1769482314283 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 @@ -4112,6 +5164,18 @@ packages: purls: [] size: 918664 timestamp: 1742083674731 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + sha256: ec37c79f737933bbac965f5dc0f08ef2790247129a84bb3114fad4900adce401 + md5: 810d83373448da85c3f673fbcb7ad3a3 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 958864 + timestamp: 1775753750179 - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda sha256: 82695c9b16a702de615c8303387384c6ec5cf8b98e16458e5b1935b950e4ec38 md5: 1819e770584a7e83a81541d8253cbabe @@ -4122,6 +5186,17 @@ packages: purls: [] size: 977701 timestamp: 1742083869897 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h8f8c405_0.conda + sha256: ae9d83cab8988a7d4ccec411fef23c141b5b3d301db3e926ab7cd4befe3764e6 + md5: f2bb6692dfb33a1bbce746aa812a9a5b + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 1007272 + timestamp: 1775754456682 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda sha256: 907a95f73623c343fc14785cbfefcb7a6b4f2bcf9294fcb295c121611c3a590d md5: 3b1e330d775170ac46dff9a94c253bd0 @@ -4132,6 +5207,16 @@ packages: purls: [] size: 900188 timestamp: 1742083865246 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda + sha256: 1a9d1e3e18dbb0b87cff3b40c3e42703730d7ac7ee9b9322c2682196a81ba0c3 + md5: 8423c008105df35485e184066cad4566 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 920039 + timestamp: 1775754485962 - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda sha256: c092d42d00fd85cf609cc58574ba2b03c141af5762283f36f5dd445ef7c0f4fe md5: b58b66d4ad1aaf1c2543cbbd6afb1a59 @@ -4143,6 +5228,17 @@ packages: purls: [] size: 1081292 timestamp: 1742083956001 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda + sha256: 7a6256ea136936df4c4f3b227ba1e273b7d61152f9811b52157af497f07640b0 + md5: 4152b5a8d2513fd7ae9fb9f221a5595d + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1301855 + timestamp: 1775753831574 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda sha256: 8f5bd92e4a24e1d35ba015c5252e8f818898478cb3bc50bd8b12ab54707dc4da md5: a78c856b6dc6bf4ea8daeb9beaaa3fb0 @@ -4154,6 +5250,19 @@ packages: purls: [] size: 3884556 timestamp: 1740240685253 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e + md5: 1b08cd684f34175e4514474793d44bcb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852330 + timestamp: 1771378262446 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda sha256: e86f38b007cf97cc2c67cd519f2de12a313c4ee3f5ef11652ad08932a5e34189 md5: c75da67f045c2627f59e6fcb5f4e3a9b @@ -4174,6 +5283,17 @@ packages: purls: [] size: 33601 timestamp: 1680112270483 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 + md5: 38ffe67b78c9d4de527be8315e5ada2c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40297 + timestamp: 1775052476770 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c md5: 5aa797f8787fe7a17d1b0821485b5adc @@ -4199,6 +5319,22 @@ packages: purls: [] size: 689316 timestamp: 1743091137869 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + sha256: 3bc5551720c58591f6ea1146f7d1539c734ed1c40e7b9f5cb8cb7e900c509aba + md5: 995d8c8bad2a3cc8db14675a153dec2b + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 hca6bf5a_0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 46810 + timestamp: 1776376751152 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.7-hebb159f_0.conda sha256: 21119df0a2267a9fc52d67bdf55e5449a2cdcc799865e2f90ab734fd61234ed8 md5: 45786cf4067df4fbe9faf3d1c25d3acf @@ -4213,6 +5349,21 @@ packages: purls: [] size: 609769 timestamp: 1743091248758 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda + sha256: 24248928e63b5de45012c8ad3fd6b350ae1fe2fc355613bb89ee5f0a35835bea + md5: 33f30d4878d1f047da82a669c33b307d + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 h7a90416_0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 40836 + timestamp: 1776377277986 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.7-h178c5d8_0.conda sha256: d3ddc9ae8a5474f16f213ca41b3eda394e1eb1253f3ac85d3c6c99adcfb226d8 md5: aa838a099ba09429cb80cc876b032ac4 @@ -4227,6 +5378,22 @@ packages: purls: [] size: 582736 timestamp: 1743091513375 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda + sha256: 4d9c117b2dd222cf891710d5f6a570ebb275479979843a1477ac54ed50907b40 + md5: 0c1fdc80534d8f25fd74722aba81f044 + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 h6967ea9_0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - icu <0.0a0 + license: MIT + license_family: MIT + purls: [] + size: 41663 + timestamp: 1776377341241 - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.7-he286e8c_0.conda sha256: 99182f93f1e7b678534df5f07ff94d7bf13a51386050f8fa9411fec764d0f39f md5: aec4cf455e4c6cc2644abb348de7ff20 @@ -4241,6 +5408,91 @@ packages: purls: [] size: 1513490 timestamp: 1743091551681 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda + sha256: da68af9d9d28d65a6916db1bef68f8a25c64c4fdcf759f32a2d2f2f143220adf + md5: e3b5acbb857a12f5d59e8d174bc536c0 + depends: + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 h692994f_0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - icu <0.0a0 + license: MIT + license_family: MIT + purls: [] + size: 43916 + timestamp: 1776376994334 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + sha256: 3d44f737c5ae52d5af32682cc1530df433f401f8e58a7533926536244127572a + md5: e79d2c2f24b027aa8d5ab1b1ba3061e7 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - libxml2 2.15.3 + license: MIT + license_family: MIT + purls: [] + size: 559775 + timestamp: 1776376739004 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda + sha256: 437f003e299d77403db42d17e532d686236f357ac5c3d6bf466558c697902597 + md5: c74ae93cd7876e3a9c4b5569d5e29e34 + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - libxml2 2.15.3 + license: MIT + license_family: MIT + purls: [] + size: 496338 + timestamp: 1776377250079 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda + sha256: 43895a7517c055b8893531290f9dc48bd751eb04be04f14bbce3b6c71b052be6 + md5: 6c8292c2ee808aeef2406083beaa6da7 + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - libxml2 2.15.3 + - icu <0.0a0 + license: MIT + license_family: MIT + purls: [] + size: 465820 + timestamp: 1776377317454 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + sha256: 8038084c60eda2006d0122d05e3364fe8db0a18935ca6ed0168b5ba5aa33f904 + md5: f7d6fcda29570e20851b78d92ea2154e + depends: + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libxml2 2.15.3 + - icu <0.0a0 + license: MIT + license_family: MIT + purls: [] + size: 518869 + timestamp: 1776376971242 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda sha256: 684e9b67ef7b9ca0ca993762eeb39705ec58e2e7f958555c758da7ef416db9f3 md5: e71f31f8cfb0a91439f2086fc8aa0461 @@ -4252,6 +5504,19 @@ packages: purls: [] size: 254297 timestamp: 1701628814990 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + sha256: 0694760a3e62bdc659d90a14ae9c6e132b525a7900e59785b18a08bb52a5d7e5 + md5: 87e6096ec6d542d1c1f8b33245fe8300 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxml2 + - libxml2-16 >=2.14.6 + license: MIT + license_family: MIT + purls: [] + size: 245434 + timestamp: 1757963724977 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.39-h03b04e6_0.conda sha256: decfc5614a10231a17543b7366616fb2d88c14be6dd9dd5ecde63aa9a5acfb9e md5: a6e0cec6b3517ffc6b5d36a920fc9312 @@ -4262,6 +5527,18 @@ packages: purls: [] size: 231368 timestamp: 1701628933115 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + sha256: 00d6b5e92fc1c5d86e095b9b6840f793d9fc4c9b4a7753fa0f8197ab11d5eb90 + md5: 367b8029352f3899fb76cc20f4d144b9 + depends: + - __osx >=10.13 + - libxml2 + - libxml2-16 >=2.14.6 + license: MIT + license_family: MIT + purls: [] + size: 225660 + timestamp: 1757964032926 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.39-h223e5b9_0.conda sha256: 2f1d99ef3fb960f23a63f06cf65ee621a5594a8b4616f35d9805be44617a92af md5: 560c9cacc33e927f55b998eaa0cb1732 @@ -4272,6 +5549,18 @@ packages: purls: [] size: 225705 timestamp: 1701628966565 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + sha256: 7a4d0676ab1407fecb24d4ada7fe31a98c8889f61f04612ea533599c22b8c472 + md5: 90f7ed12bb3c164c758131b3d3c2ab0c + depends: + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + license: MIT + license_family: MIT + purls: [] + size: 220345 + timestamp: 1757964000982 - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda sha256: 6e3d99466d2076c35e7ac8dcdfe604da3d593f55b74a5b8e96c2b2ff63c247aa md5: 279ee338c9b34871d578cb3c7aa68f70 @@ -4285,6 +5574,20 @@ packages: purls: [] size: 418542 timestamp: 1701629338549 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + sha256: 13da38939c2c20e7112d683ab6c9f304bfaf06230a2c6a7cf00359da1a003ec7 + md5: 46034d9d983edc21e84c0b36f1b4ba61 + depends: + - libxml2 + - libxml2-16 >=2.14.6 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 420223 + timestamp: 1757963935611 - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 md5: edb0dca6bc32e4f4789199455a1dbeb8 @@ -4298,6 +5601,18 @@ packages: purls: [] size: 60963 timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 63629 + timestamp: 1774072609062 - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda sha256: 8412f96504fc5993a63edf1e211d042a1fd5b1d51dedec755d2058948fcced09 md5: 003a54a4e32b02f7355b50a837e699da @@ -4310,6 +5625,18 @@ packages: purls: [] size: 57133 timestamp: 1727963183990 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + sha256: 4c6da089952b2d70150c74234679d6f7ac04f4a98f9432dec724968f912691e7 + md5: 30439ff30578e504ee5e0b390afc8c65 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 59000 + timestamp: 1774073052242 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b md5: 369964e85dc26bfe78f41399b366c435 @@ -4322,6 +5649,18 @@ packages: purls: [] size: 46438 timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 47759 + timestamp: 1774072956767 - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 md5: 41fbfac52c601159df6c01f875de31b9 @@ -4336,6 +5675,20 @@ packages: purls: [] size: 55476 timestamp: 1727963768015 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 + md5: dbabbd6234dea34040e631f87676292f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 58347 + timestamp: 1774072851498 - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py312he28fd5a_0.conda sha256: 4f3a78b59890f2175a381d9ae5e74b4523aea23daaa01cafbb150456bc8b857c md5: 52d16dd592060d4b2fa9ad325e0c1f90 @@ -4368,6 +5721,23 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1404411 timestamp: 1739211853813 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py314hae3bed6_0.conda + sha256: 8edbaad598410cd3a9b69e94281eaa5f8632585c882618e61a690975031e4a25 + md5: 8115b838f94eac2cef6bff236c4d25f4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause and MIT-CMU + purls: + - pkg:pypi/lxml?source=hash-mapping + size: 1581460 + timestamp: 1776512598345 - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py312h91b2f42_0.conda sha256: 9d8caf0b13e42a214f47f21e4a4696a7de37e81b182fb88c0e922b5940fb716e md5: a1b3a0206fc6c434fadc25d818002b82 @@ -4398,6 +5768,22 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1258398 timestamp: 1739212205592 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py314h1d4708b_0.conda + sha256: 9f47ec5a688eef6a645853b218dc653a2e969f22368498379b93c263635ec69d + md5: 29604e0a4d440ed409009b8aaad74e59 + depends: + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause and MIT-CMU + purls: + - pkg:pypi/lxml?source=hash-mapping + size: 1417422 + timestamp: 1776512991743 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py312h9535dd2_0.conda sha256: b899871ecf3f331e3047295897809758a02a144e4118f1378ca443c62772cd2c md5: f9d4307bbe7d394ac3634fe85a4c0e94 @@ -4430,6 +5816,23 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1219666 timestamp: 1739211889959 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py314h264e108_0.conda + sha256: 85e3c1874326e4681d5dcfc48273fcfcd653b6f3681d7e15e0890e69e01db0f5 + md5: c11f8cea755d00e8389aebdc069156bd + depends: + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause and MIT-CMU + purls: + - pkg:pypi/lxml?source=hash-mapping + size: 1375410 + timestamp: 1776512951800 - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py312h53bce91_0.conda sha256: 78519f3a92e8e284792b9b13d4240643b47b3c1902b2288e2a4dfeb83f78e787 md5: c86f153c26b4d6235de9e19eafc01ce8 @@ -4464,6 +5867,24 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1051362 timestamp: 1739212280294 +- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py314hcdb55d9_0.conda + sha256: b6ac0a0ef4da0cdfaff9d1204160e2e88d57fe9d884ff2491bb5ef78fba9cd02 + md5: 0171707b7a0f89c0daa9e75d39ce0ca8 + depends: + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause and MIT-CMU + purls: + - pkg:pypi/lxml?source=hash-mapping + size: 1239608 + timestamp: 1776512723280 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda sha256: 0fbacdfb31e55964152b24d5567e9a9996e1e7902fb08eb7d91b5fd6ce60803a md5: fee3164ac23dfca50cfcc8b85ddefb81 @@ -4476,6 +5897,18 @@ packages: - pkg:pypi/markdown-it-py?source=hash-mapping size: 64430 timestamp: 1733250550053 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + sha256: 7b1da4b5c40385791dbc3cc85ceea9fad5da680a27d5d3cb8bfaa185e304a89e + md5: 5b5203189eb668f042ac2b0826244964 + depends: + - mdurl >=0.1,<1 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/markdown-it-py?source=hash-mapping + size: 64736 + timestamp: 1754951288511 - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda sha256: 4a6bf68d2a2b669fecc9a4a009abd1cf8e72c2289522ff00d81b5a6e51ae78f5 md5: eb227c3e0bf58f5bd69c0532b157975b @@ -4508,6 +5941,22 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24856 timestamp: 1733219782830 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf + md5: 9a17c4307d23318476d7fbf0fedc0cde + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 27424 + timestamp: 1772445227915 - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py312h3520af0_1.conda sha256: d521e272f7789ca62e7617058a4ea3bd79efa73de1a39732df209ca5299e64e2 md5: 32d6bc2407685d7e2d8db424f42018c6 @@ -4538,6 +5987,21 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24363 timestamp: 1733219815199 +- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py314h77fa6c7_1.conda + sha256: 74507b481299c3d35dc7d1c35f9c92e2e94e0eda819b264f5f25b7552f8a7d64 + md5: 5d45a74270e21481797387a209b3dec3 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 26740 + timestamp: 1772445674690 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda sha256: 4aa997b244014d3707eeef54ab0ee497d12c0d0d184018960cce096169758283 md5: 46e547061080fddf9cf95a0327e8aba6 @@ -4570,6 +6034,22 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24757 timestamp: 1733219916634 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + sha256: 411153d14ee0d98be6e3751cf5cc0502db17bce2deebebb8779e33d29d0e525f + md5: d33c0a15882b70255abdd54711b06a45 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 27256 + timestamp: 1772445397216 - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda sha256: bbb9595fe72231a8fbc8909cfa479af93741ecd2d28dfe37f8f205fef5df2217 md5: 944fdd848abfbd6929e57c790b8174dd @@ -4604,6 +6084,23 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 27930 timestamp: 1733220059655 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + sha256: 02805a0f3cd168dbf13afc5e4aed75cc00fe538ce143527a6471485b36f5887c + md5: 8de7b40f8b30a8fcaa423c2537fe4199 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 30022 + timestamp: 1772445159549 - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda sha256: 69b7dc7131703d3d60da9b0faa6dd8acbf6f6c396224cf6aef3e855b8c0c41c6 md5: af6ab708897df59bd6e7283ceab1b56b @@ -4628,6 +6125,18 @@ packages: - pkg:pypi/mdit-py-plugins?source=hash-mapping size: 42180 timestamp: 1733854816517 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + sha256: 123cc004e2946879708cdb6a9eff24acbbb054990d6131bb94bca7a374ebebfc + md5: 1997a083ef0b4c9331f9191564be275e + depends: + - markdown-it-py >=2.0.0,<5.0.0 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mdit-py-plugins?source=hash-mapping + size: 43805 + timestamp: 1754946862113 - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 md5: 592132998493b3ff25fd7479396e8351 @@ -4686,6 +6195,25 @@ packages: - pkg:pypi/mypy?source=hash-mapping size: 17058016 timestamp: 1738767732637 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py314h5bd0f2a_0.conda + sha256: ff90f20225011cbf8e5ae9b97e1b445135b19e633b5d3b0320e02d763a324054 + md5: 06b2a90aa4363ac50c959926bc384dea + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 + - psutil >=4.0 + - python >=3.14,<3.15.0a0 + - python-librt >=0.8.0 + - python_abi 3.14.* *_cp314 + - typing_extensions >=4.6.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=compressed-mapping + size: 20297545 + timestamp: 1776802022720 - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py312h01d7ebd_0.conda sha256: 38132c4b5de6686965f21b51a1656438e83b2a53d6f50e9589e73fb57a43dd49 md5: 0251bb4d6702b729b06fd5c7918e9242 @@ -4718,6 +6246,24 @@ packages: - pkg:pypi/mypy?source=hash-mapping size: 11022410 timestamp: 1738768159908 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py314h217eccc_0.conda + sha256: 7a9e6f0b4c13622a3fba514472bee5130e9254fc2c8ef689b9ff9783f50492c8 + md5: bcf7143c9f4b1bd763d715815fb98fd9 + depends: + - __osx >=11.0 + - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 + - psutil >=4.0 + - python >=3.14,<3.15.0a0 + - python-librt >=0.8.0 + - python_abi 3.14.* *_cp314 + - typing_extensions >=4.6.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + size: 13069509 + timestamp: 1776803940390 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py312hea69d52_0.conda sha256: 7284d77173d385f5c7456c13d825dbae170920a31ca7a0996d2608ad17f17e2f md5: 909034322685579577b1bbb9b47e39e1 @@ -4752,6 +6298,25 @@ packages: - pkg:pypi/mypy?source=compressed-mapping size: 10275919 timestamp: 1738768578918 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py314hbdd0d06_0.conda + sha256: 4753e5a40d6cc0ba0fbf42b43b639cd4fcc2fadbeed6571589c6dc6b8c856671 + md5: 19e51df1d38f05a2384771cdcbe355a7 + depends: + - __osx >=11.0 + - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 + - psutil >=4.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python-librt >=0.8.0 + - python_abi 3.14.* *_cp314 + - typing_extensions >=4.6.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=compressed-mapping + size: 12162223 + timestamp: 1776802958871 - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py312h4389bb4_0.conda sha256: 3bab35d2f17f9b2c8498c952f7d182848f2d70775e7e970d5f53c7eeb87741a6 md5: 1eea4f4c0038b6f9b399dfad2305cd6f @@ -4788,6 +6353,26 @@ packages: - pkg:pypi/mypy?source=hash-mapping size: 8300827 timestamp: 1738768501453 +- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py314h5a2d7ad_0.conda + sha256: 1480cdef0b28c52494432df2deb2c379d5892e04695ca823217c79860cd71bdf + md5: c9dffe30976574155a3c77eafc3ba15f + depends: + - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 + - psutil >=4.0 + - python >=3.14,<3.15.0a0 + - python-librt >=0.8.0 + - python_abi 3.14.* *_cp314 + - typing_extensions >=4.6.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=compressed-mapping + size: 9760683 + timestamp: 1776802300096 - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda sha256: 1895f47b7d68581a6facde5cb13ab8c2764c2e53a76bd746f8f98910dc4e08fe md5: 29097e7ea634a45cc5386b95cac6568f @@ -4799,6 +6384,17 @@ packages: - pkg:pypi/mypy-extensions?source=hash-mapping size: 10854 timestamp: 1733230986902 +- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 + md5: e9c622e0d00fa24a6292279af3ab6d06 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy-extensions?source=hash-mapping + size: 11766 + timestamp: 1745776666688 - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda sha256: f035d0ea623f63247f0f944eb080eaa2a45fb5b7fda8947f4ac94d381ef3bf33 md5: b528795158847039003033ee0db20e9b @@ -4816,6 +6412,23 @@ packages: - pkg:pypi/myst-parser?source=hash-mapping size: 73074 timestamp: 1739381945342 +- conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + sha256: f352d594d968acd31052c5f894ae70718be56481ffa9c304fdfcbe78ddf66eb1 + md5: a65e2c3c764766f0b28a3ac5052502a6 + depends: + - docutils >=0.20,<0.23 + - jinja2 + - markdown-it-py >=4.0.0,<4.1.0 + - mdit-py-plugins >=0.5,<0.6 + - python >=3.11 + - pyyaml + - sphinx >=8,<10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/myst-parser?source=hash-mapping + size: 73535 + timestamp: 1768942892170 - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda sha256: a20cff739d66c2f89f413e4ba4c6f6b59c50d5c30b5f0d840c13e8c9c2df9135 md5: 6bb0d77277061742744176ab555b723c @@ -4886,6 +6499,16 @@ packages: purls: [] size: 891641 timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause + purls: [] + size: 918956 + timestamp: 1777422145199 - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda sha256: ea4a5d27ded18443749aefa49dc79f6356da8506d508b5296f60b8d51e0c4bd9 md5: ced34dd9929f491ca6dab6a2927aff25 @@ -4895,6 +6518,15 @@ packages: purls: [] size: 822259 timestamp: 1738196181298 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + sha256: f5f7e006ff4271305ab4cc08eedd855c67a571793c3d18aff73f645f088a8cae + md5: 31b8740cf1b2588d4e61c81191004061 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 831711 + timestamp: 1777423052277 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 md5: 068d497125e4bf8a66bf707254fff5ae @@ -4904,6 +6536,15 @@ packages: purls: [] size: 797030 timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d + md5: 343d10ed5b44030a2f67193905aea159 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 805509 + timestamp: 1777423252320 - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 md5: 598fd7d4d0de2455fb74f56063969a97 @@ -4939,6 +6580,18 @@ packages: purls: [] size: 2939306 timestamp: 1739301879343 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb + md5: da1b85b6a87e141f5140bb9924cecab0 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3167099 + timestamp: 1775587756857 - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda sha256: 505a46671dab5d66df8e684f99a9ae735a607816b12810b572d63caa512224df md5: a7d63f8e7ab23f71327ea6d27e2d5eae @@ -4950,6 +6603,17 @@ packages: purls: [] size: 2591479 timestamp: 1739302628009 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + sha256: 334fd49ea31b99114f5afb1ec44555dc8c90640648302a4f8f838ee345d1ec50 + md5: 5cf0ece4375c73d7a5765e83565a69c7 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 2776564 + timestamp: 1775589970694 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda sha256: 4f8e2389e1b711b44182a075516d02c80fa7a3a7e25a71ff1b5ace9eae57a17a md5: 75f9f0c7b1740017e2db83a53ab9a28e @@ -4961,6 +6625,17 @@ packages: purls: [] size: 2934522 timestamp: 1739301896733 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea + md5: 25dcccd4f80f1638428613e0d7c9b4e1 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3106008 + timestamp: 1775587972483 - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda sha256: 56dcc2b4430bfc1724e32661c34b71ae33a23a14149866fc5645361cfd3b3a6a md5: 0730f8094f7088592594f9bf3ae62b3f @@ -4974,6 +6649,19 @@ packages: purls: [] size: 8515197 timestamp: 1739304103653 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 + md5: 05c7d624cff49dbd8db1ad5ba537a8a3 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9410183 + timestamp: 1775589779763 - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c md5: e51f1e4089cad105b6cac64bd8166587 @@ -4997,6 +6685,18 @@ packages: - pkg:pypi/packaging?source=hash-mapping size: 60164 timestamp: 1733203368787 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 + md5: 4c06a92e74452cfa53623a81592e8934 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 91574 + timestamp: 1777103621679 - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f md5: 457c2c8c08e54905d6954e79cb5b5db9 @@ -5030,6 +6730,17 @@ packages: - pkg:pypi/pathspec?source=hash-mapping size: 41075 timestamp: 1733233471940 +- conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + sha256: 6eaee417d33f298db79bc7185ab1208604c0e6cf51dade34cd513c6f9db9c6f3 + md5: 11adc78451c998c0fd162584abfa3559 + depends: + - python >=3.10 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/pathspec?source=compressed-mapping + size: 56559 + timestamp: 1777271601895 - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a md5: d0d408b1f18883a944376da5cf8101ea @@ -5096,6 +6807,18 @@ packages: - pkg:pypi/pluggy?source=hash-mapping size: 23595 timestamp: 1733222855563 +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pluggy?source=hash-mapping + size: 25877 + timestamp: 1764896838868 - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda sha256: 454e2c0ef14accc888dd2cd2e8adb8c6a3a607d2d3c2f93962698b5718e6176d md5: c64b77ccab10b822722904d889fa83b5 @@ -5149,6 +6872,20 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 475101 timestamp: 1740663284505 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda + sha256: f15574ed6c8c8ed8c15a0c5a00102b1efe8b867c0bd286b498cd98d95bd69ae5 + md5: 4f225a966cfee267a79c5cb6382bd121 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 231303 + timestamp: 1769678156552 - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py312h01d7ebd_0.conda sha256: bdfa40a1ef3a80c3bec425a5ed507ebda2bdebce2a19bccb000db9d5c931750c md5: fcad6b89f4f7faa999fa4d887eab14ba @@ -5175,6 +6912,19 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 482494 timestamp: 1740663492867 +- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py314hd330473_0.conda + sha256: 3194ce0d94c810cb1809da851261be34e1cae72ca345445b29e61766b38ee6cc + md5: d465805e603072c341554159939be5b8 + depends: + - python + - __osx >=10.13 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 242816 + timestamp: 1769678225798 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda sha256: cb11dcb39b2035ef42c3df89b5a288744b5dcb5a98fb47385760843b1d4df046 md5: 0f461bd37cb428dc20213a08766bb25d @@ -5203,6 +6953,20 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 484139 timestamp: 1740663381126 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda + sha256: e0f31c053eb11803d63860c213b2b1b57db36734f5f84a3833606f7c91fedff9 + md5: fc4c7ab223873eee32080d51600ce7e7 + depends: + - python + - __osx >=11.0 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 245502 + timestamp: 1769678303655 - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda sha256: 088451ee2c9a349e1168f70afe275e58f86350faffb09c032cff76f97d4fb7bb md5: f5b86d6e2e645ee276febe79a310b640 @@ -5233,6 +6997,21 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 491314 timestamp: 1740663777370 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda + sha256: 17c8274ce5a32c9793f73a5a0094bd6188f3a13026a93147655143d4df034214 + md5: fd539ac231820f64066839251aa9fa48 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 249950 + timestamp: 1769678167309 - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 md5: 7d9daffbb8d8e0af0f769dbbcd173a54 @@ -5284,6 +7063,25 @@ packages: - pkg:pypi/pydata-sphinx-theme?source=hash-mapping size: 1547597 timestamp: 1734446468767 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + sha256: 71161705133df512054177ad03f394e073c39e369dda52fda8e8e0a5371df8c2 + md5: 620cee61c85cf6a407f80e8d502796ec + depends: + - accessible-pygments + - babel + - beautifulsoup4 + - docutils !=0.17.0 + - pygments >=2.7 + - python >=3.10 + - sphinx >=7.0 + - typing_extensions + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pydata-sphinx-theme?source=hash-mapping + size: 1657335 + timestamp: 1776777605561 - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda sha256: 28a3e3161390a9d23bc02b4419448f8d27679d9e2c250e29849e37749c8de86b md5: 232fb4577b6687b2d503ef8e254270c9 @@ -5295,6 +7093,17 @@ packages: - pkg:pypi/pygments?source=hash-mapping size: 888600 timestamp: 1736243563082 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 + md5: 16c18772b340887160c79a6acc022db0 + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=compressed-mapping + size: 893031 + timestamp: 1774796815820 - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-11.0-py312h2365019_0.conda sha256: 91a27ede294fec129d115f2e0b0ce881f0c12332ee5e9c33ba522c037ad14bbb md5: 0925c0e6ee32098c461423ea93490b97 @@ -5389,6 +7198,25 @@ packages: - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping size: 384331 timestamp: 1736927195004 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.21.1-py310hd8a072f_0.conda + noarch: python + sha256: b2c870bbb3b8390ee2ff286f36e9cf8c2a112996874c64832929a9781c336db7 + md5: 7c3414d8e19b34fa2f65f1f89aad3c66 + depends: + - python + - toml-fmt-common + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyproject-fmt?source=hash-mapping + size: 4238093 + timestamp: 1776177970104 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.5.1-py39h77e2912_1.conda noarch: python sha256: e54f69d8635daa04c0dbb868bc8eece8a8db61d231babd8887051227dc63651c @@ -5408,6 +7236,24 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 1603752 timestamp: 1740150937392 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.21.1-py310hb9b2626_0.conda + noarch: python + sha256: 6aa524f2fe6d86011b0100a1dd12dbdb311c725bd0f8ef5248868b5f41fb308a + md5: 2a98231f3db20270ce32f9458c2b3dbf + depends: + - python + - toml-fmt-common + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyproject-fmt?source=hash-mapping + size: 4127911 + timestamp: 1776178140236 - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.5.1-py39h286ba15_1.conda noarch: python sha256: 1f6687a6aa09ff76a7a47cc95f9a2da43ef58f50ecfeafd32962edd0391d4fd6 @@ -5426,6 +7272,24 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 1504931 timestamp: 1740150987346 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.21.1-py310h3b8a9b8_0.conda + noarch: python + sha256: 22cb944b345d38e5ce18c95b9b41f6a816c09d172af1b0586e1f8b1de431c6a0 + md5: fb3a4b2b6086d6d69aa60d4b86550d0e + depends: + - python + - toml-fmt-common + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyproject-fmt?source=hash-mapping + size: 3879847 + timestamp: 1776178129040 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py312h6f6235b_0.conda sha256: 7f3e81bf127c894140bf608f71e3f96d902eeaf2525b766b0f99a04f6371084e md5: f64845e77fa7c6262abd68e190989a7f @@ -5460,6 +7324,24 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 1184361 timestamp: 1730382555877 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.21.1-py310ha413424_0.conda + noarch: python + sha256: c7056dcd5cb72512299a997861f01b9d2e54edceac9d4d88e9f1035d764754ea + md5: 23e365f5895e29b0bcac1d0007ff116a + depends: + - python + - toml-fmt-common + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - _python_abi3_support 1.* + - cpython >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyproject-fmt?source=hash-mapping + size: 4260906 + timestamp: 1776178177936 - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.5.1-py39he870945_1.conda noarch: python sha256: 4cc2b5024b7dac334eedab339e11bf78180dcad71e31f3090982645f9d4d6bfc @@ -5525,6 +7407,27 @@ packages: - pkg:pypi/pytest?source=hash-mapping size: 259816 timestamp: 1740946648058 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + sha256: 960f59442173eee0731906a9077bd5ccf60f4b4226f05a22d1728ab9a21a879c + md5: 6a991452eadf2771952f39d43615bb3e + depends: + - colorama >=0.4 + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=compressed-mapping + size: 299984 + timestamp: 1775644472530 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda sha256: 09acac1974e10a639415be4be326dd21fa6d66ca51a01fb71532263fba6dccf6 md5: 79963c319d1be62c8fd3e34555816e01 @@ -5539,6 +7442,21 @@ packages: - pkg:pypi/pytest-cov?source=hash-mapping size: 26256 timestamp: 1733223113491 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + sha256: 44e42919397bd00bfaa47358a6ca93d4c21493a8c18600176212ec21a8d25ca5 + md5: 67d1790eefa81ed305b89d8e314c7923 + depends: + - coverage >=7.10.6 + - pluggy >=1.2 + - pytest >=7 + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest-cov?source=compressed-mapping + size: 29559 + timestamp: 1774139250481 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda sha256: a6af87cdb4cd981b33707147fc0ed37a5e4ea8322283a014947bccdfeff57a99 md5: 010e50e74c467db278f1398a74106a04 @@ -5553,6 +7471,20 @@ packages: - pkg:pypi/pytest-html?source=hash-mapping size: 25315 timestamp: 1734739529167 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda + sha256: 1b5441efb96fa7bf0a442b32b76c7adc37702b1589dfff10b80beb665e1ff76a + md5: 12eaff6b42a938530c0c49a310260b2d + depends: + - jinja2 >=3.0.0 + - pytest >=7.0.0 + - pytest-metadata >=2.0.0 + - python >=3.10 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/pytest-html?source=hash-mapping + size: 26178 + timestamp: 1768989647804 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 sha256: ed916397b9caec080b929d24c62a91654fd829b6d6569ccd573cb2aeb12e70aa md5: 837e335fa428cf7c784ee2e80594506c @@ -5593,6 +7525,21 @@ packages: - pkg:pypi/pytest-xdist?source=hash-mapping size: 38147 timestamp: 1733240891538 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + sha256: b7b58a5be090883198411337b99afb6404127809c3d1c9f96e99b59f36177a96 + md5: 8375cfbda7c57fbceeda18229be10417 + depends: + - execnet >=2.1 + - pytest >=7.0.0 + - python >=3.9 + constrains: + - psutil >=3.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest-xdist?source=hash-mapping + size: 39300 + timestamp: 1751452761594 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda build_number: 1 sha256: 77f2073889d4c91a57bc0da73a0466d9164dbcf6191ea9c3a7be6872f784d625 @@ -5648,6 +7595,34 @@ packages: size: 33233150 timestamp: 1739803603242 python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + build_number: 100 + sha256: dec247c5badc811baa34d6085df9d0465535883cf745e22e8d79092ad54a3a7b + md5: a443f87920815d41bfe611296e507995 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libuuid >=2.42,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 36705460 + timestamp: 1775614357822 + python_site_packages_path: lib/python3.14/site-packages - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.9-h9ccd52b_1_cpython.conda build_number: 1 sha256: c394f7068a714cad7853992f18292bb34c6d99fe7c21025664b05069c86b9450 @@ -5695,6 +7670,31 @@ packages: size: 13961675 timestamp: 1739802065430 python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.4-h7c6738f_100_cp314.conda + build_number: 100 + sha256: fc99d7a6a3f5eb776c20880c441e3708ff95d35d0a03f3ceb2a89016f59a01fc + md5: d4e8506d0ac094be21451682eed9ce4d + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 14431104 + timestamp: 1775616356805 + python_site_packages_path: lib/python3.14/site-packages - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.9-hc22306f_1_cpython.conda build_number: 1 sha256: fe804fc462396baab8abe525a722d0254c839533c98c47abd2c6d1248ad45e93 @@ -5742,6 +7742,31 @@ packages: size: 11682568 timestamp: 1739801342527 python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + build_number: 100 + sha256: 27e7d6cbe021f37244b643f06a98e46767255f7c2907108dd3736f042757ddad + md5: e1bc5a3015a4bbeb304706dba5a32b7f + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 13533346 + timestamp: 1775616188373 + python_site_packages_path: lib/python3.14/site-packages - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.9-h3f84c4b_1_cpython.conda build_number: 1 sha256: 320acd0095442a451c4e0f0f896bed2f52b3b8f05df41774e5b0b433d9fa08e0 @@ -5789,6 +7814,31 @@ packages: size: 16848398 timestamp: 1739800686310 python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + build_number: 100 + sha256: e258d626b0ba778abb319f128de4c1211306fe86fe0803166817b1ce2514c920 + md5: 40b6a8f438afb5e7b314cc5c4a43cd84 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + - python_abi 3.14.* *_cp314 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 18055445 + timestamp: 1775615317758 + python_site_packages_path: Lib/site-packages - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda sha256: a50052536f1ef8516ed11a844f9413661829aa083304dc624c5925298d078d79 md5: 5ba79d7c71f03c678c8ead841f347d6e @@ -5832,6 +7882,16 @@ packages: purls: [] size: 47795 timestamp: 1739800832944 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + sha256: 36ff7984e4565c85149e64f8206303d412a0652e55cf806dcb856903fa056314 + md5: e4e60721757979d01d3964122f674959 + depends: + - cpython 3.14.4.* + - python_abi * *_cp314 + license: Python-2.0 + purls: [] + size: 49806 + timestamp: 1775614307464 - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda sha256: 4790787fe1f4e8da616edca4acf6a4f8ed4e7c6967aa31b920208fc8f95efcca md5: a61bf9ec79426938ff785eb69dbb1960 @@ -5843,6 +7903,62 @@ packages: - pkg:pypi/python-json-logger?source=hash-mapping size: 13383 timestamp: 1677079727691 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py314h0f05182_0.conda + sha256: 91e220ec82b60899d522c9ce8d1d3aba727ace155c27ef0ad58efe39417bc94f + md5: 3de7ef40dc693af4681d4998297f0052 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 79052 + timestamp: 1775764036701 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py314h0b69929_0.conda + sha256: b7414d48343fc05bb5f0bb035f3b66ebf6443b4b0b58d194e2a911680b76711a + md5: b5c0be3b92113c71e422833bbd251df1 + depends: + - python + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 70114 + timestamp: 1775764122931 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py314ha14b1ff_0.conda + sha256: c68f760966344b6e7e31d7fe2dd90ebdcd4701413cdf2b058da29ff727673884 + md5: b409c6b041d02d3795f981f870dc5013 + depends: + - python + - __osx >=11.0 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 74109 + timestamp: 1775825547864 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py314hc5dbbe4_0.conda + sha256: 4dd14f2461aa91ec9904a9411025e8c9c91308ea47655d07bc89d3a9d2847f73 + md5: 05ea58209be41c7152956f36a496e040 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 51707 + timestamp: 1775764106960 - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda build_number: 5 sha256: d10e93d759931ffb6372b45d65ff34d95c6000c61a07e298d162a3bc2accebb0 @@ -5865,6 +7981,17 @@ packages: purls: [] size: 6217 timestamp: 1723823393322 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + build_number: 8 + sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 + md5: 0539938c55b6b1a59b560e843ad864a4 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6989 + timestamp: 1752805904792 - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda build_number: 5 sha256: 4da26c7508d5bc5d8621e84dc510284402239df56aab3587a7d217de9d3c806d @@ -6023,16 +8150,31 @@ packages: md5: 50992ba61a8a1f8c2d346168ae1c86df depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - libgcc >=13 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 205919 + timestamp: 1737454783637 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d + md5: 2035f68f96be30dc60a5dfd7452c7941 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 205919 - timestamp: 1737454783637 + size: 202391 + timestamp: 1770223462836 - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312h3520af0_2.conda sha256: de96d83b805dba03422d39e855fb33cbeedc8827235d6f76407a3b42dc085910 md5: 4a2d83ac55752681d54f781534ddd209 @@ -6061,6 +8203,20 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 196573 timestamp: 1737455046063 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py314h10d0514_1.conda + sha256: aef010899d642b24de6ccda3bc49ef008f8fddf7bad15ebce9bdebeae19a4599 + md5: ebd224b733573c50d2bfbeacb5449417 + depends: + - __osx >=10.13 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 191947 + timestamp: 1770226344240 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda sha256: ad225ad24bfd60f7719709791345042c3cb32da1692e62bd463b084cf140e00d md5: 68149ed4d4e9e1c42d2ba1f27f08ca96 @@ -6091,6 +8247,21 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 194243 timestamp: 1737454911892 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + sha256: 95f385f9606e30137cf0b5295f63855fd22223a4cf024d306cf9098ea1c4a252 + md5: dcf51e564317816cb8d546891019b3ab + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 189475 + timestamp: 1770223788648 - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda sha256: 76fec03ef7e67e37724873e1f805131fb88efb57f19e9a77b4da616068ef5c28 md5: ba00a2e5059c1fde96459858537cc8f5 @@ -6123,6 +8294,22 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 182783 timestamp: 1737455202579 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + sha256: a2aff34027aa810ff36a190b75002d2ff6f9fbef71ec66e567616ac3a679d997 + md5: 0cd9b88826d0f8db142071eb830bce56 + depends: + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 181257 + timestamp: 1770223460931 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.4.0-py312hbf22597_0.conda sha256: 65a264837f189b0c69c5431ea8ef44e405c472fedba145b05055f284f08bc663 md5: fa0ab7d5bee9efbc370e71bcb5da9856 @@ -6235,6 +8422,18 @@ packages: purls: [] size: 282480 timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda sha256: 53017e80453c4c1d97aaf78369040418dea14cf8f46a2fa999f31bd70b36c877 md5: 342570f8e02f2f022147a7f841475784 @@ -6245,6 +8444,17 @@ packages: purls: [] size: 256712 timestamp: 1740379577668 +- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + sha256: 4614af680aa0920e82b953fece85a03007e0719c3399f13d7de64176874b80d5 + md5: eefd65452dfe7cce476a519bece46704 + depends: + - __osx >=10.13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 317819 + timestamp: 1765813692798 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 md5: 63ef3f6e6d6d5c589e64f11263dc5676 @@ -6255,6 +8465,17 @@ packages: purls: [] size: 252359 timestamp: 1740379663071 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda sha256: e20909f474a6cece176dfc0dc1addac265deb5fa92ea90e975fbca48085b20c3 md5: 9140f1c09dd5489549c6a33931b943c7 @@ -6287,6 +8508,24 @@ packages: - pkg:pypi/requests?source=hash-mapping size: 58723 timestamp: 1733217126197 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + sha256: 7f2c24dd3bd3c104a1d2c9a10ead5ed6758b0976b74f972cfe9c19884ccc4241 + md5: 9659f587a8ceacc21864260acd02fc67 + depends: + - python >=3.10 + - certifi >=2023.5.7 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - urllib3 >=1.26,<3 + - python + constrains: + - chardet >=3.0.2,<8 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=compressed-mapping + size: 63728 + timestamp: 1777030058920 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 md5: 36de09a8d3e5d5e6f4ee63af49e59706 @@ -6310,6 +8549,16 @@ packages: - pkg:pypi/rfc3986-validator?source=hash-mapping size: 7818 timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + sha256: 30f3c04fcfb64c44d821d392a4a0b8915650dbd900c8befc20ade8fde8ec6aa2 + md5: 0dc48b4b570931adc8641e55c6c17fe4 + depends: + - python >=3.10 + license: 0BSD OR CC0-1.0 + purls: + - pkg:pypi/roman-numerals?source=hash-mapping + size: 13814 + timestamp: 1766003022813 - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda sha256: 0116a9ca9bf3487e18979b58b2f280116dba55cb53475af7a6d835f7aa133db8 md5: 5f0f24f8032c2c1bb33f59b75974f5fc @@ -6453,6 +8702,22 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 8872400 timestamp: 1742584319600 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.11-h7805a7d_0.conda + noarch: python + sha256: cdbe0e611cf6abfea4d0a8d31721cdd357987ebc4521392638d7b57169422968 + md5: 67a5122f008a689124eeb2075c1d92ab + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 9327937 + timestamp: 1776378777189 - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py312ha54e1fc_0.conda sha256: 972a8192ec8f73d20f8e665c4cafd5aeefdc8bd8adbfdb83fc1c2bd02598d3cb md5: dcc943dc0c72dbd74524c0e410243204 @@ -6485,6 +8750,21 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 8171146 timestamp: 1742584829512 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.11-h16586dd_0.conda + noarch: python + sha256: 4b9adce4d8d99bf5f193a8bf3b2aaa91f3b65d88fd610f61a6330120704eacaf + md5: d2c7c98d69f8e0d9160257fb590ffe4f + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=hash-mapping + size: 9350619 + timestamp: 1776378920511 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py312h31a5b27_0.conda sha256: d8ec16cdee63ab6279f2f174344563e0eef5597167bfe3b1d4001b5c9f140187 md5: 04650bb002095ba4918311d035c167b2 @@ -6519,6 +8799,21 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 7801806 timestamp: 1742584905314 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.11-hc5c3a1d_0.conda + noarch: python + sha256: 2c8d24c58059cc1ed590276591634482fe921d2542957323caaa21e053cf6971 + md5: 4fe5ced33c7d002ccdf4c49c754f45c1 + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 8510514 + timestamp: 1776378932502 - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py312hc33538c_0.conda sha256: 3444f42e218faa07de917de7aeec08a16a3dba9a855aabe6f72ea721792edc3d md5: ab488dc1f8101c17d0fc4ff938860cec @@ -6549,6 +8844,21 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 7912086 timestamp: 1742585732752 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.11-h02f8532_0.conda + noarch: python + sha256: 29b1d24ad55d68abe04ff7911107344e63d3b76ae54f58c52a2a74fbf8a53c4c + md5: ce7fdb3d4e42746ae13703ae80176c75 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 9828825 + timestamp: 1776378829267 - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda sha256: 00926652bbb8924e265caefdb1db100f86a479e8f1066efe395d5552dde54d02 md5: 938c8de6b9de091997145b3bf25cdbf9 @@ -6636,6 +8946,17 @@ packages: - pkg:pypi/snowballstemmer?source=hash-mapping size: 58824 timestamp: 1637143137377 +- conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + sha256: 17007a4cfbc564dc3e7310dcbe4932c6ecb21593d4fec3c68610720f19e73fb2 + md5: 755cf22df8693aa0d1aec1c123fa5863 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/snowballstemmer?source=hash-mapping + size: 73009 + timestamp: 1747749529809 - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda sha256: 54ae221033db8fbcd4998ccb07f3c3828b4d77e73b0c72b18c1d6a507059059c md5: 3f144b2c34f8cb5a9abd9ed23a39c561 @@ -6647,6 +8968,17 @@ packages: - pkg:pypi/soupsieve?source=hash-mapping size: 36754 timestamp: 1693929424267 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + sha256: 23b71ecf089967d2900126920e7f9ff18cdcef82dbff3e2f54ffa360243a17ac + md5: 18de09b20462742fe093ba39185d9bac + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/soupsieve?source=hash-mapping + size: 38187 + timestamp: 1769034509657 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda sha256: 995f58c662db0197d681fa345522fd9e7ac5f05330d3dff095ab2f102e260ab0 md5: f7af826063ed569bb13f7207d6f949b0 @@ -6675,6 +9007,34 @@ packages: - pkg:pypi/sphinx?source=hash-mapping size: 1424416 timestamp: 1740956642838 +- conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + sha256: 035ca4b17afca3d53650380dd94c564555b7ec2b4f8818111f98c15c7a991b7b + md5: aabfbc2813712b71ba8beb217a978498 + depends: + - alabaster >=0.7.14 + - babel >=2.13 + - colorama >=0.4.6 + - docutils >=0.21,<0.23 + - imagesize >=1.3 + - jinja2 >=3.1 + - packaging >=23.0 + - pygments >=2.17 + - python >=3.12 + - requests >=2.30.0 + - roman-numerals >=1.0.0 + - snowballstemmer >=2.2 + - sphinxcontrib-applehelp >=1.0.7 + - sphinxcontrib-devhelp >=1.0.6 + - sphinxcontrib-htmlhelp >=2.0.6 + - sphinxcontrib-jsmath >=1.0.1 + - sphinxcontrib-qthelp >=1.0.6 + - sphinxcontrib-serializinghtml >=1.1.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/sphinx?source=hash-mapping + size: 1584836 + timestamp: 1767271941650 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda sha256: d7433a344a9ad32a680b881c81b0034bc61618d12c39dd6e3309abeffa9577ba md5: 16e3f039c0aa6446513e94ab18a8784b @@ -6758,6 +9118,19 @@ packages: - pkg:pypi/ssort?source=hash-mapping size: 28731 timestamp: 1735848317236 +- conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + sha256: fbbf7244ab5e0c369073c61d34ddcc5999130e620b4bcae3edeb15e262fa52a7 + md5: 77f7731cbc8b1e17c86a9756fc914c7f + depends: + - pathspec >=0.9.0 + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/ssort?source=hash-mapping + size: 30640 + timestamp: 1765229793979 - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 md5: b1b505328da7a6b246787df4b5a49fbc @@ -6826,6 +9199,20 @@ packages: - pkg:pypi/tinycss2?source=hash-mapping size: 28285 timestamp: 1729802975370 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e md5: d453b98d9c83e71da0741bb0ff4d76bc @@ -6847,6 +9234,28 @@ packages: purls: [] size: 3270220 timestamp: 1699202389792 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + sha256: 7f0d9c320288532873e2d8486c331ec6d87919c9028208d3f6ac91dc8f99a67b + md5: 6e6efb7463f8cef69dbcb4c2205bf60e + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3282953 + timestamp: 1769460532442 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 md5: b50a57ba89c32b62428b71a875291c9b @@ -6869,6 +9278,18 @@ packages: purls: [] size: 3503410 timestamp: 1699202577803 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: TCL + license_family: BSD + purls: [] + size: 3526350 + timestamp: 1769460339384 - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda sha256: 34f3a83384ac3ac30aefd1309e69498d8a4aa0bf2d1f21c645f79b180e378938 md5: b0dd904de08b7db706167240bf37b164 @@ -6892,6 +9313,19 @@ packages: - pkg:pypi/toml-fmt-common?source=hash-mapping size: 13323 timestamp: 1735486723150 +- conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + sha256: 84df8bf7b7359aad0bae13c8ffd8b2bb869bdc434d660c51620283f7339e7ba8 + md5: efa7b8a03b79b5a249ca23821dad2e3a + depends: + - python >=3.10 + - tomli >=2.4 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/toml-fmt-common?source=hash-mapping + size: 15589 + timestamp: 1774040339912 - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda sha256: 18636339a79656962723077df9a56c0ac7b8a864329eb8f847ee3d38495b863e md5: ac944244f1fed2eb49bae07193ae8215 @@ -6903,6 +9337,18 @@ packages: - pkg:pypi/tomli?source=hash-mapping size: 19167 timestamp: 1733256819729 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd + md5: b5325cf06a000c5b14970462ff5e4d58 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=hash-mapping + size: 21561 + timestamp: 1774492402955 - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda sha256: c96be4c8bca2431d7ad7379bad94ed6d4d25cd725ae345540a531d9e26e148c9 md5: c532a6ee766bed75c4fa0c39e959d132 @@ -7020,6 +9466,16 @@ packages: purls: [] size: 10075 timestamp: 1733188758872 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 91383 + timestamp: 1756220668932 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda sha256: 337be7af5af8b2817f115b3b68870208b30c31d3439bec07bfb2d8f4823e3568 md5: d17f13df8b65464ca316cbc000a3cb64 @@ -7031,6 +9487,18 @@ packages: - pkg:pypi/typing-extensions?source=hash-mapping size: 39637 timestamp: 1733188758212 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c md5: f6d7aa696c67756a650e91e15e88223c @@ -7049,6 +9517,13 @@ packages: purls: [] size: 122968 timestamp: 1742727099393 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda sha256: db8dead3dd30fb1a032737554ce91e2819b43496a0db09927edf01c32b577450 md5: 6797b005cd0f439c4c5c9ac565783700 @@ -7058,6 +9533,16 @@ packages: purls: [] size: 559710 timestamp: 1728377334097 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 694692 + timestamp: 1756385147981 - pypi: ./ name: upstage-des version: 1.0.0 @@ -7104,6 +9589,33 @@ packages: - pkg:pypi/urllib3?source=hash-mapping size: 100102 timestamp: 1734859520452 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + sha256: af641ca7ab0c64525a96fd9ad3081b0f5bcf5d1cbb091afb3f6ed5a9eee6111a + md5: 9272daa869e03efe68833e3dc7a02130 + depends: + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + size: 103172 + timestamp: 1767817860341 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a + md5: 1e610f2416b6acdd231c5f573d754a0f + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 19356 + timestamp: 1767320221521 - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda sha256: 908bd027ce305bd585e1662efba3085be7376b2d81bdc84d7c39311298f28de8 md5: 632e2d558d7e9f7762b03366d615fbd3 @@ -7128,6 +9640,31 @@ packages: purls: [] size: 751016 timestamp: 1743121397932 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 + md5: 37eb311485d2d8b2c419449582046a42 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_34 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 683233 + timestamp: 1767320219644 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 + md5: 242d9f25d2ae60c76b38a5e42858e51d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 115235 + timestamp: 1767320173250 - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.42.34438-h7142326_25.conda sha256: 688f9bd904754bf5ddda7c602e43ee544a2f23c005bb6a6f4603082b7f998285 md5: 946b28e9f15703e4afc591a6183e18a2 @@ -7200,6 +9737,17 @@ packages: license_family: MIT purls: [] size: 1176306 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + purls: [] + size: 85189 + timestamp: 1753484064210 - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae @@ -7218,6 +9766,16 @@ packages: purls: [] size: 84237 timestamp: 1641347062780 +- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + sha256: a335161bfa57b64e6794c3c354e7d49449b28b8d8a7c4ed02bf04c3f009953f9 + md5: a645bb90997d3fc2aea0adf6517059bd + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + purls: [] + size: 79419 + timestamp: 1753484072608 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 sha256: 93181a04ba8cfecfdfb162fc958436d868cc37db504c58078eab4c1a3e57fbb7 md5: 4bb3f014845110883a3c5ee811fd84b4 @@ -7226,6 +9784,31 @@ packages: purls: [] size: 88016 timestamp: 1641347076660 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac + md5: 78a0fe9e9c50d2c381e8ee47e3ea437d + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 83386 + timestamp: 1753484079473 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 + md5: 433699cba6602098ae8957a323da2664 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 63944 + timestamp: 1753484092156 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 sha256: 4e2246383003acbad9682c7c63178e2e715ad0eb84f03a8df1fbfba455dfedc5 md5: adbfb9f45d1004a26763652246a33764 @@ -7422,3 +10005,49 @@ packages: - pkg:pypi/zstandard?source=hash-mapping size: 449910 timestamp: 1741853538921 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + sha256: 47101a4055a70a4876ffc87b750ab2287b67eca793f21c8224be5e1ee6394d3f + md5: 727109b184d680772e3122f40136d5ca + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 528148 + timestamp: 1764777156963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 433413 + timestamp: 1764777166076 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 + md5: 053b84beec00b71ea8ff7a4f84b55207 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 388453 + timestamp: 1764777142545 diff --git a/pixi.toml b/pixi.toml index 672fda6..687c7f1 100644 --- a/pixi.toml +++ b/pixi.toml @@ -113,12 +113,26 @@ features = [ "tasks-test", ] +[environments.py314] +features = [ + "py314", + "deps-lint", + "deps-test", + "deps-docs", + "tasks-lint", + "tasks-test", +] + + [feature.py312.dependencies] python = "3.12.*" [feature.py313.dependencies] python = "3.13.*" +[feature.py314.dependencies] +python = "3.14.*" + [feature.deps-lint.dependencies] mypy = "*" pyproject-fmt = ">=2.5" diff --git a/src/upstage_des/actor.py b/src/upstage_des/actor.py index e19ae9b..e449141 100644 --- a/src/upstage_des/actor.py +++ b/src/upstage_des/actor.py @@ -16,7 +16,6 @@ from upstage_des.base import ( SimulationError, UpstageBase, - UpstageError, ) from upstage_des.root_types import StateDataDict from upstage_des.states import LinearChangingState, State, _ActiveState diff --git a/src/upstage_des/states.py b/src/upstage_des/states.py index f24b645..a2f7471 100644 --- a/src/upstage_des/states.py +++ b/src/upstage_des/states.py @@ -7,13 +7,15 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Union, cast, get_args, get_origin +from typing import TYPE_CHECKING, Any, TypeVar, Union, cast, get_args, get_origin from upstage_des.base import SimulationError if TYPE_CHECKING: from upstage_des.actor import _BaseActor as Actor +T = TypeVar("T") + def check_type(value: Any, annotation: Any) -> bool: """Check if a value matches the given type annotation. diff --git a/src/upstage_des/tasks.py b/src/upstage_des/tasks.py index 0254438..a564514 100644 --- a/src/upstage_des/tasks.py +++ b/src/upstage_des/tasks.py @@ -7,7 +7,7 @@ from collections.abc import Generator from enum import IntFlag -from typing import TYPE_CHECKING, Any, TypeVar +from typing import Any, TypeVar from warnings import warn from simpy import Environment as SimpyEnv From b87ea88a4f4df3d1535b54f011c110f9cece2cca Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 17:42:39 -0400 Subject: [PATCH 04/12] Basic docs for CI. Init and version. --- docs/source/_static/.gitignore | 9 +++ docs/source/_static/favicon.ico | Bin 0 -> 15086 bytes docs/source/_static/upstage-flow.png | Bin 0 -> 18453 bytes docs/source/_static/upstage-logo-medium.png | Bin 0 -> 1113 bytes docs/source/_static/upstage-logo-small.png | Bin 0 -> 805 bytes docs/source/conf.py | 77 ++++++++++++++++++ docs/source/index.md | 60 ++++++++++++++ src/upstage_des/__init__.py | 85 ++++---------------- src/upstage_des/_version.py | 9 +++ 9 files changed, 172 insertions(+), 68 deletions(-) create mode 100644 docs/source/_static/.gitignore create mode 100644 docs/source/_static/favicon.ico create mode 100644 docs/source/_static/upstage-flow.png create mode 100644 docs/source/_static/upstage-logo-medium.png create mode 100644 docs/source/_static/upstage-logo-small.png create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.md create mode 100644 src/upstage_des/_version.py diff --git a/docs/source/_static/.gitignore b/docs/source/_static/.gitignore new file mode 100644 index 0000000..58960de --- /dev/null +++ b/docs/source/_static/.gitignore @@ -0,0 +1,9 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore +!*.html +!*.css +!*.js +!*.png +!*.ico \ No newline at end of file diff --git a/docs/source/_static/favicon.ico b/docs/source/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3269b79c529dafcd6f4b68822c79c60fa89800d5 GIT binary patch literal 15086 zcmeHOOK%)S5U#wi$(sc7A`cTgQC=83yFI;rBrzc|u@|BUDLf($0VEWZM1VpBNI;Sc zca9vmaN?`f&G=}^2a4sx4vL?{a#}6X5IsC^wBPw z&a({99$c*X#c1qSrk}1PV%M|p`nWBAq0_XZYWO_Eo073x8NR#LEyD-*SWiV`7yNaa ztQ^1QLr(bB@~iCKyS*Yq3UqY;g|8DetV7ehhMP*cBS8mw3(p& z$p4D=RfV2wyxtkR9pC!Lw^=o=I$;<5ni`eiS9~xd{wAw`_F-;(-Wk6~P1w+I6*92m z=?wUrn5Xs%4!%pPVHandWvJ0;+4y;khk(85h`-5G{A>^dbh;7q%Yu^(kTux0jQ?T# z`}1PA{9#Av12*nbG;Ox+%AiO`c4~ zE<9y^2mZ6EGL+~~h%Kk+z6tws_P=<7wgNx&kzz}e^h_2#$JK@9)Z+2uyC~ij*Ea4s z#=>YS{u2BNzRYX5zKnUp`ZYnC54)8KV*z6-;Ty<&RnVVc_1CUQ?3x$xU3OY5c_@$3 z5Z2rCi85$1Ye=5{r46e`6J7XAz;5xtj-F1$k8fn_W~EHLioKeVC625iB zcvf`U*2_XMFP<~_8@wRjB1_vmbs@vm*UG{k|SP&$gsG zRlmbn==QahImWG;b!q)$%);*q%twkYh<%mDpyG}qeeEgg*wX#+OPB`(zIFkB zqA-}89pGFs=xaB5frfH+)wgCHzP6%+eFKt{amd#`m9v|^He>83{rL!ML|?(=WDYw* z>;ZFXMt>qFyV9E(Ut7~Ge2h%~MCm{ZbZC5SS<4*#i2|%DYl^Y32X^#AWej@IG5m{T zkCQ(UR*CVoGwy4`eGKjg8W?C`pn)s2<^KXPTdps`&Lh6PJOf9>f>w)c*t4vIoG-;<$dSW^Q|kEKi25U+x797i z+BWcyj{jmT+Ip@O@vH-pvMX3*TkddQ;4gWD-MI`oa;Rc*6Y3Iw3-h)2W*_X)R3wHB zIZ&|sb=|oIj}m$N7?9@!o15>#kRgYuD>p!5?Fn*Q-j;&@&My8La*)`Atk&*j-p|0> z$AEF)g~1`_*NV-X7!B5ue7pq%Y(eIQQymx_a)2an!N^`mWHE=_R(^wFoHk%yxX^(i zWp4`p6Q&g8ZE?UB_J;V!=HQ6Dm{E@vmD}>R6goh>zdbJ`%*SRfFG+68Td0XSXhME& z8$(GsFhL$^|6`5Vf^V^CF!U+r1m4dX{*r^zrDc8x=7r(MG4ACe+n`Q14aljAe?7dzyo@_J zdg;+6=M-`*F@oZsQ&jq^1KGDc9EFcH$KIc3S$}$+_05miyLN;1j(Ox&p8%A^3w{dn z$L;k9o23+-zXXLjWouGh{v cIUIhT<=*H5dx|V=s;JvK`^4CVvF9oO1Jqh>;s5{u literal 0 HcmV?d00001 diff --git a/docs/source/_static/upstage-flow.png b/docs/source/_static/upstage-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..68e48427670c5e5e58a11da4105ce23afb4500cb GIT binary patch literal 18453 zcma%@WmFtZ^yW!|OVA*}-Q5Z97F-6mV8JaoB)Gc-4*`P1;0_tw9R_!I32xiG|J^Tp z&YrU$kfFP)tGl|Z?tPx$C0s>G1`YWmG7Jn1nw+ep8u0T72Ih@0B0TV0jM`%m@Z*h( znv6J1#VFA}@B!9R?6VjQOjRt(qX``F8Oc#r#{~xFefR6vn`FlKgfK8NC32Et8Xktn zSxA|mw_o}?Y8-x=^tLA+Rcn6B^~0i&lM0@sVUA$>fR*qi{I`nEWHv2)7aab`vgZVQtVC;w?4TcX4{J%S8 z0&By@he!zugoxqbL*Pr4u|7~^5!q5_(q_8n?&71~N&UcJ`mG?#M~_XbltldLIWF#! zNTu5o0i7ht$nQ=nAfQY#5E4p{5rBq{r_=5eKAgd;zBBB3P8xuQ*e?i=VtXp_ODZy= z_2t>;ZuLb=PH|X-nJXYB3?n>hSa2o8^`hp|T*Mkauz?;UmViR9_13`iYR27svzIif zZiJ7L?D_)()x~)q>5$8q5G^5(Z4#I*-uK~p==NNF`dnmiOr%ot!7#~bvv=^%=Zt^+ zi0s_l28N#Ie-v|s4U{tZihC5=`(jD8#r>5daL8ACE-r>RgD~fNuaEf2 z3zf1GN-Mq=F`1WPZ421R&8PnSNr6TeE9k-THPJ3v8S96t4Bc zKP-ONz3)Vya9Ka6>r<@ywHWG6CDLMlxjwREIbxxM9)lD6AO_iBlkQFp-du z5`KB4Y})^`a_+C<%n>$SJqk9$ch{R>h-6Mbn?e z>KM=Xcno+X_eh-prk)> z@qA!mDp~O^h$0mc=VelLAQtihH!E4TkwQMA-u(bthx0wIv!1NCLpUBK;ZNAVsJETh zN@4QU)-g&I{HNl-Ke>%ttX>#Yo%BY&^(mQ;^Wj@UqLhGpyJ}>;Y`hBf227fecS`-w z&EC(dccBbbA@UQygY#AM7|s8FN2pcEOi3wWI>5BcLJMi_W+yitoQX)`n4zGeR0#XI zw>R8Ow^0MRm`s*rA4VMxI&%A@%Dpl)VeRrKwJ$xTNxjXTQ*9D51wJ<79>Y#fsU z1@$NFdv7)S(MU#--|#JTzWwcl$C&@eVwT=cbzwF&dUH9F&XCgBTf}sPB}?$=5<}?j zB?E*8ue|92T=Db&nYb?BYLc?3%}pDjHT5CRF49$ngv#^>zJnk7W5lBy4&T*d0-J#l z6a+g&s~{3@F_JDfl)}8-g)UNijgN{Kf?3w!G(nt1C-TxNX6Us~Ehj4**Y5iuzde|2 z-W`VN97JOkxGu;ZF!FA4YLXhHGylo{v^?mchDonpk)`9&nR)GCrc|N9etGM#vZFqR z2j96Dktf#psOt07pU>ON?yJefpT70KLl-0ydA`llcVBj!1e-_zGl+H%sxQ}8keRfA zroD>BAk=Zq6og4Z+76bfcuMU7<1*_~v9_KCWQ&p#5YRU)IY$a!^i!%;nMxfZ213XK z%$o*;J|!gU5ja{js;W4GyCwVfbhv4;lw>cgLp~YY%9vOv0XtJuQ)>c~jD3Mu%=f=A z{EdgTOPuW&o@k$!n%zoqPh?1a^9!HW1%SS7I*)KeK{EJ{1Tu&zJ(+167TCDBR0H$` zlf`B-^R1pWt-qK04+UyPIG2xoCe-&PC-c*sRTpdR7!wPX;`-3z@DoGU_wGcfLwhE48p= zd@^;E%W8Jp5_!3cRrE5cPTtY;xt_PYc)T1NQWkm2M=oV+S`Q)Invfw){yI~l1N0TH zk6XaRvqZC$B^14HXrb=VnSNFg{SzCR?7Z;m6N7;39$BeYd4k*FtXwLK!B!x;a2#Vr zqxb?tB@8>^BZ3T4N6zbF2yw3-l`I|>LhgWxgsJ-3r}%MPKH4Qgn+zh&boIK2K@8H{BKxRu1Hb33ky8f1AK z6c=v6nsA}U^jN|TIkuHnG|RU@q|5O7h~1ZR+ptpTi?u7WPoM=~ole_s4))^Vch?gG zq{^a>iZzN8FiR1vW=fKo44O3}Q|-w^#3o}$^53=F1iGB@LoQU2;9{h;aJ;s!1{A&=tC z?)Bb;1g}cl<>;qbRd}OrOIQUcnklnkYh#%~i&nrA0y<&qTeIRc(D$A!9=mjsIPs^H zANZV>qqQ*{w^+jDpb$>=vEN&LpE;k(Jjd)BmjBQ~JD#o=)yhEZcAnu8=SMYT+1#nn zxQ^Q(V?thgCTnZ!g=z|2iD#$dI_Mzps!yI?%Dz$tn@NDZvgqG3xi9@j7Y1N1kG1!l z4522W@T)MfmOT+4ye8kcI1_Y5eczulS8O2n@7IRJ5b>+=a;X~q?wDEg=CT?W)zfPw z)lJ$R$tZkqMnL;WC3oFS6Cy~t3aiku7RlOPgiy*EG`hA(HVK?n{U&nrL)-KHVROt- z89Ao{-Dy7_WJB%hbk#@(HD9%BjR=F@KxEbTCX35qMPH`-m;cQGZBA_rn@6G#Ceu60 zJ@H>LUmR&9TZ}25UY>764EnN!eVcJlj^=C1DN$F*#^4^#y3h~Bo*u!$pzfJ9tDn@h z9_OZa7elOdSH4G8efhxjs5=ZlmRQK1Wrbw%$nc{(+wb0*k=uWL)9Bm0xz`cL1TtDv zU_H{#E|%kW2IbS0HaG1oN{P@W$P)qr!lCBfWuo8*rgx+JL4BR~$kO2KA0f1o@`~oa z1vtuCxw$o!a)irEMU+CvL?P-w3vMps1zp1Xq6qRRm#3JG={(3(c!Cu2C~OePwTuzJ z_}Qzfsy34i*sliLwu@#j^#Bv|m&ts&$wFn)qGz}!u9Y;2M?baP~=L9W1qv>!?oDO>6kiOcMY z!dR6+u-qFby;Ad&+r=T4M5qK#((HrTASQdK^qwLzIR|_&4BWGxLtBAfcG`T;AVOwW z9mp~j8j4OjTXE|D@?hkvT>I?8vN3$HOy0@wvLg%3IId%GaJ@aOoTpg3}BrH7+0 zF=9WaQ4ZH#5>_IATWWLy#uM9mx+p%`l&*-3sBAe`BGh%d_x9FV+KeYClG^8V#3}YMiI)B)N+~K{j~bK3hcCR<49yGE*loBQUNkAW7;KC ziCND)qL6Y%Eu{f}6z&_}?*DDJgo2B!?J-@X+I;qR#A2w6WSrl-HWM}aigE=O{+p2{3hVfIAme*YUi=rCV<)kBPF@9Gb8{VonOI}gwIjbeN)v) zG7b?Rlg&4wS7v3AGRN=fN?WKFvkJzV{|CrPlRwpqbz?*DW8&P;5`LdsM|z#=tu*ns zKb(Y^Xo3;ny5lW3RrK)>KW%5HwJIxGj*dCBQh>BH@+2>k*F)h?17+ot8K{?A+@Y$B zdm*{U0n)@wB#dV!<_@jr(!^_-H`5LT?osAFlT9dLH}%QzG}Dx_7x~IL;%`)EDh%|L z&$~p&eT`icYgsw|`GA|;HCazVQJTq?O!(ejj;jvrTL;Epq&}mEn8)W6s z@$`*3?CIg=u$O8>mfTd-7^KHU-nlJgDwz}(+3WyBY3945S-Sm1kp%_8_;1vVdXXm= z6rNIXEe`UhOT#D6LynEv?$Ay)YhOahkbL_i zbrzKCYm>vZ+rSVD+0o4sVde3|mdAy8PUV^tWb$TEprDZr{srPUF4Q(3V2T01%nRx` ziW!QGe&}})Jit;X&EJ8*23y!0 zAolt7a!*Kt&2(2$j5ecG=R2cU_l z`7df!kTtubZMi3>WWQL%6r`TNqF)0ci)`jG6R!V8fCe#{uxG3tITWNnUPLIcwoQZP zB7+-0Bc_bn1(LIp1)&5!KcNBbLneAVKeqPZ8TsS}mSf(8|6St}V1J*Pye?-3iz7De z_i)U67!?pM1OFoOZ(PDlmspi1Glbyr6cu!J1_V^lWr4p$pq)yN;Mi4+K!^rkKpSRu z?XL|?6b08e6g)uB15CohlHh4+;yL-VkoKd1<1g**e&t^I)yfQCQ8j; zaFGtAnL}Zi#6~~vJj^&PZ48Gl5V%EqInUa#2k+x4@w-M#&fth+5pfYktl2!U^;a_- z+V7Yc6`qp8tk1><L0F^oHv4eM+GlWeV@G{5^)=Qe^a;`|UIC|2>)tMCdIIza zo^dfx27MX9Mo-&6LOk9EqUOIH$;nbhUio*BLPIjr&3rnsV~riM;wwxC?znmOwl4Hov>}Xb%n;%4&tLKyw^PXfpTNR zJf>-w2U{y4m6^Gjp67SADZ>kl-03)pjc+H*)yEN|F?4ti9jv|8%iLSsex7Vu|74Lm zbTPCAX{9V%3h5z79L1b(^$*WT}{jy%G^_^NrikOr)M4FO`XFZ5C>+osZ_&g_cH)!c|T{wnrijORnXV8l7>~ zK|{e2DeQdutIu+`dbN8m9U^#1!NU?_vMV7r7^40I2|b=Px*gu~l+#mpKzb!RmMwJU zt+m|s;To?r^+UJ(w3x8JhX=T1sIR!z_{k4gZ}FG`-OlgHT^`Ix%{47Ah)zK%Rk>o6 zEAA<$?7r{w(^##;&p@+4mcP*I4r$)LN>ASA#zt*V?G5|)Ko`V0^V8irwD{})QrPy}->5)LFCG1{Ag_O_Uy6)1 zg=@=8N;CdsMYHEA4&6VT^~);yt|KeaSi9_1x6=sQA>I~#h@JEMyZeP!laGC;O}iup zg~DE3pONyayym9%(kMNMlcFxVSF8MFl}}H!dzf z;`4j5(Fxo0X1S?Z6!Ji?#iaE#qCqAS=_|5j9Xk)Hz7Q6E4g+En-Bpx6GUov1gw#*h zLT~zta!!YLf31PnFB5sKul*h-5Rs9EFMZAp96#TP#~57m+cps!Z6yY5Lrxd!O0(Zg zSDDE?1-=I9C}Gerb0D4(aP*w7Ha8#s$$qb3Cc?>Q-?$Dh6dCq}80RxLX44(~6Ih=M zdOqjEAfn+_hy}t6w3!d3c%i9{ZsMN=BE474wh+lR;D609ILvxctpOFrFn_ z<_cd{AO(`?J7wvZenFf=;;X#TFPlF+jwL|x_2qKxplT( z0?Q1an%a494v;tIn=V+`f>yD`6%~f!4BNc+U4Uz5sj(cJfWy4Ge^pC?*_QRR^|AuX zud({se5R&K!k>k(TRC_9x0E`8(7kmQ?QVtcRe?siuGFe_JO$>(qJ!SQI#%4AIgb)WXJP;={Ikrj=>TyAT*rut8ObQnYsbdopcG{SofTs(y~IXuf1*hrx`Ohm!$A=>&sGZb7l(cMAvZt- zPl-loSlnGL`%>}U-SyF<8cUf~r}>{AzsFgaVg`@$lhU$VHVWP-^hTx0nVWiJ>fSFg zajMSoBUhvT@}=<8eO@f?{$mVrq&D0*wQjfXy=y$pXWauAzq{=?tfR;h`}<&AcUMym?;F~L*QGabmqcS=RmtFv^0-7%4uvYsyFB|t1wFSxQ1*!c82*XbPw91Dx; z27je^FtV-(Y?^eDASxOmj|Tb*!?J*)Kpz=LrJ9YapL>sW~WVQ-_J1_K%zQb zrlTI(L%`?w>!hWiv{xAS$Myb{j0otuO{3D#0Po`x?MC0{!rE;F*SBHinx#V;xs5Wu ztEWZc2Po$B5nmOC+Z9-fD_X-fy>d#vGC)P9e!oF?7VmUwy4xX<{}M}&GH0N7;vZP~ z--1a>-N`S&Oh%@ncLGGBmDN7M)4pZn(52da+ABgTdiLX4XrH?5=N<*V-<1V}rUu`W znDtb{Z&|60RV$tY7cvBVZsYEkG29dGN6`Dfzqz4z7no9ePsk%Z^LceiQ!@k*IH^chr-8AR7%38Zc)oc&bU+pwGg3q1ZngdNG7#-IWFEZ z{F`5^3E2K2t)GT29l~NxKj1lH#B{Xk|5C2!?ZoS(V>c9Rymu(c<_JhNRud+2m?RJQ zdt!mucX70)zboWO^fO#Ae-Y=7DQ)^;ShsvTuEoa`a^oJ|>@%yW9CB-9WnzOvE`1Ps z`Zu+3aGRju!vbczS$zRt2zxQ2oHzLGO--s@JzbtP*e%NI%u`o?JATm+BcKd~N1NI` zJLd~IHnO!84gzMmYZ`5d>>dY253)1DwYD`T}}DJhwBf2^4$ zTG;fDQ%XQjLnDac=flAmIz4^i%+n)1_t$A*7xZt!fT9Oj2>GynbqhuYzIc6_hG~8J zsKSgO@L?=s!ae0|G;0hz(d=*P+u8nj%DN1%sn5Jwn1qcl+o9E~mI!aVsw?ywlp>P^ z=*05}vgPjTVV1=jzr9({cAtf24k4Dz*H|8Qz8Pw_o}vMyPz0N??7#y*f0e7*o9qX>asIDQC{vE?>Lr!cfjBuWFRLmud%B7Cv6Q970sO6 z0YxBbR1P;QWAHgBLHOh%WX!Z50EaStdLC2)Rf-QKCCML^(@x3H#wA>RGh1rf(|O>1 ztMa4-Eopvv&%EOjpO8>CsyxGD*%fkCSFqT8rJZG&NP|`Ys@j;WwdQmiDIRnqF3{iA zOOhH9O!Y1?dOZ6!jziR97BP-It#bBwj`j8++GK-+_BQ6@uVDF^h=aMg zs>JD25jnufO8|~m_Z2?eiX^kbdHUF74CuKfhUeumUv}v|_C)UYD3?nj5kahQCKoU& z&M7Pgx()rWG5`uv)kIi#)pr{`;DNT4iZnMXRm}^j|8vdWsIoQS-EP$#JkuLof0IaS zAggm(pb+ZCW?byFTGSF3B1-?IzYthen+d~P&qyvN2kjr&#Wn_|WWTtAoBbKtQI?cI zpvLK92?ftE;TVMT5W*9clQYAsahXyCuas)p0!^`R#m|}X@lr`{0)Grvb?aEv(RU=> zl)zqb1FW)xN%X7*@|tlM+X^KBl^1lS!LsC& zHJ0w%7KRKh?k~l#?%@n<()*8d7*};r!}5c&nWPWP1*7Om($VQfW7xk0L)bKv_kW!f zXD;}MH{Z}{igf4-slPe6vo1mYaSbf+8C%suEUAD;^))s1&;+`%Az#FxVwd$Y53X!% zjy)K3PVmPo2>$?tb?R3_;~;7GdCVtYKyp>BXuaUjgfY+FSBksO_Sf!E-Cl^t(&5{+ z*wROT_TNdfbj6FLMR=im!M75YoyfPidtdX_xkY36V&2$mXrJty3I2;q2&W#zVds-66N#(Nq57 z?m0n!s`qX?f3J{o1*EQKXQ1k6=%jtV&yA0SFXm>;#y^Kh!chMvCt!?f4F1I%B?Lw7 zE9W5G`FUK6gWw|~!B8;U(v)2)Ut#nk->6?~5ysDBCCib-k#FJ4Q**L4QdYZ%*&*MX zl!XyI9nmIzG3_HmIS!A{qq3GuwxIKE)V52n% zo}#QAvUZ3MVX%74U9o1n6_5raw`k58pSeXDfB2Rf{XZKqn3nnA11rs7OMlt)k-`#U z;Ge~E8w~Fd626n)5`=G0vnPg2oNRh|H5TaM(6uT{HCUxL21Dd}J(-%f4q-#t=7N7< z5Nw!og?1q!p(o8a94D4K2)j4LV3q~Q!6_PBihGG|KbXsGjTtHJuRAM+IyEiUJ4_{V z>(*Npw`?6Dp(w#q8%~wx<1i9i6g*;6f_mC+Ru}=de_Z_*b=3G`8cE<_8!#N@;)_6x zb!&mUM>lL#@dCE912FKUG?bF!fq7Kiq3rE&A1Fa^<=v>WyaC+*UF^;>hviJvH)oQj zvI+k8z{c}{H1Z_3(Q=4;Rd9N$ON8$m;E(aBKYl8}TYl&Lf`EKR3)~t>f8!Virx>)+ zzqn;pU_Zq3U?e)FB*{R|U-to^aNG@L;Fe-Ay_xrWd2(m$Svwrx`EI9icQB7nZRw`RA28 zYCC#B!7*f&Euc;m?0O6?NrN=xDj+x%0g=5{#EUVk4FoG$Qkg5@%D1S))Fj&Y3IAgj zh&S}t2K;J>TR_@7p!r>W=K<`8m%(}I4Iezy8UY2LvD;}F@LIp9&XixD(f#53Xuj1kX*_}z) zsO=V?HG1795gjY)l;tdk?c*t=%5YY<0HJ+W6~}t6$gr(B`D>#J;^6K8XzbdhKOKUY zqIr7E?bC*&NtJD9L@t9egm>;h7>Tz?8a8UH2i{%q$(k+KvuW^ne!SC)ST|j35YE|M z_u;)Tjf{zzMN`5@R^G{3mVVyl0Nshj1Z+za+y#<(W=-`XNHoqLyR2ru4IcAXYiE<4a5BjyX(=YjdzZ>DAOo)v~RbfX+UG*-@)qH(HP=SWjI|u-%AP9xwJ$O zs(MVR{zY}Zr4p=;>+b!Kigj=oMz1Ct&N_vL_C7MC#E#WnJQ&T4rLIUZ!#T8yMa^+pSBa00^LsK<-pP&Csj7#Hfx>0x zDXNx>UbO}=Qx|2@H5n_jomR^`v(&-2=e0+|_j`G|MRtqzGtml&*yQM>!uqigsZ3M> zvU=7B{M`YJ4uI1+@D004R2FthvI2ZrMj*dQq|Nr|lxNq+IG9I@q@#M^^`ptkfF_Y$ zJtkH9mix49<~#ESQkSUwWXj?fFX@$%SNT%w+U}j-Z5gBxgmr!znG>(1+{)k(-TL`= zlFAV80aM2NCMp*Be5mFJYO<1aTVY(9=sXE($>UgtZ^=Z+tTYqk){`|75}v1ht6Pwm z=owBzimX$ALw(f=EFvr#Fy>l^IabOoeTcjgJX**PPOM)4cD16VjD zF^`lD}@lGx03@=PO!?xB7C^$a(%Y9R`LRxv1JKWDQN zh{a#*9CUOi-?mIge@?vWQ>Wr)mQd=cOxYNap%x+o$>Y6GFnJ_XwbXO}`NE9z2lTdIvuBvu%6Kj5nX`ZQ$ zU7(+u%W?k9gXRC2KW#NF8*@5OJ20pz!+Hs5i$o&OSo$mXLYYrQRwev|Y(`_o^0XI!PjlS46qW zrLvhDC=XdLBB3B9)#gtf5DaI6Ns3S7P59}dMu=bNs00!JriHmap*+CWo@(eNr}qn(zHKmD z0^KY|W`a@hzODsw{O(Zwg3oBDl3oo&*ZzeN#h3caTR3r`bBYA zez!WNT9$g1N%3-8!QUC!BDBgWO%9>`M%M9Ic_(>AB@&hTZK^D#iz<=48cqAb(?TKe z2>T(`vQPI$+wr@ZmoXT>9?m%A1xR$^(M3ofYLXp|xGlx*o9 z6MiU*`p7L0Fet{`8-m&1C;R}B7l%s3H!jcqNLXB$m_N@Pt5C=6lCM)J%;9#=<@wnD ze6e#;zJW|fg&woVN*dj0a6&9#NE1syXXh=*9INCh9g&}@HKtrbMSp0OI8T=J+q}PX zBsW-IZZo2%V6~igAfG(DzP-Jwq3neFpM-MwAUp3usHIj6CKlGW?}oywI|cC(yBw-zNi**J%A9@%i~q^o-- z3^T_6MQ%MgUxyC6Q2~qRCE}{!TG@n2V5aD%yIq`cv2pUG8ZXgE3ZF#Qjv z>Hg2BeBCXlyGzjkSU9}+p^wn`xF{}ZG1)rZt9*}KW7Uv*)Z$|6$yyhYre?3A$7bJD z4f}5yt@oXI3E#vu)01=W%nsq_-KpdTp(U&{@^}f7n#LMMszwaGQgX6Zql~|Ixp#>k zm6wxysTT(+#8(z8$WN_RI)9r+?{`++#E(`P>wW!Q%ZWQ(%vKP&Eodd1x5lxB8qD3{ z`5jBb3e;GwF?cySCU!s&%1y>5+d1Qcg|U`^v)%k>KZWw3gBZRI;j5m;b7aZ^o_OZg6Jg8PgNn*`#4NHvDgLY5)*N|DEq*l?#P+4!lCE(lJEz z=z?e8E8y{~Lp>+IaUZPy#m*yl>5U?2UMoZ=J--jI@;ynD0ynJ#c#11Mbk1v+)b(^_ zS+mwk6$)Zxup_=P&cqS7LnpQ{_Co*oNr;r|NFHvaaakuQrg2D-_}!WqjId=A zQqDyNrOBe;GL18@0T@EH8X8{bbg}v?($~B=R*R5HDh+P>J}--kI$vj#>Uz8|BhQ_Q z&I-5{B;+#hu(f6#{qP|>SO@x12fA%ub+5e#c8CneradT5YxBODJpr#jvIktj2>K`s zT_wd>+Ddxc12-Tqw8T`0Ak@B*Ey`G^+bmXy1_;omezyVKTA4=Rr{Pq9&1fZYw`V#8 zu*5^Lla3cZ171qUtPc3*Q6vj{6IB9ZiMCZoCwzz({y{CQgBgBaN5xA*GC+_R-qdi^ zM3w+3HgMjzu9r-@wX(_wv}Nxn_#QDsa5*gR0RwS@Gs`g;t*HxfRsrlveWf!?Y+C)j zqx_SCz9r2wXG?fEmXOy!@HM`3AZ8gJdHp${QD248y`492U?6#iVCZ|Ld6-p0Mn=lK zr9++5Hh`rKWxn8%lCSUUdOQ`H6#Ujsm|cZw$9?3H(G(s(iuL6^_3$j_%@oLQrf3Lc>p zzFm7q92^hk2XvQl>sQh-X6FZ7HowasB^P85IK@ax(c6d9j>;)3m4nreju=VtwwZl( zdS6V+IYC%eia7uc)Ciq7sh~y|auG>WG?pp+=N85${-)b^QB~?zP;xMz&6Fn*@(CDE zc35YdIdE=_sjV*&NutEY16OB_&2D(G>hh@*JY0je?;u#MR_yJC?Bd!l0E>#P$W z9w=#OB$>_T_YhmOQM+R>J7wb2+MbaBJ0MEnA+dK&x0V@~e>+)9vs0*M6%+N);vUvr zgDL&QHaVo2eEGUx_ba#rcn=F$m}@bfwJZ&{+OUjWUtfc6tq6wzvT(Ky68-2gF}(Lq znHU^7Km0&}y8v=Z3ED=V;Mjj^e>j;MN@c@3m^;sNW?&afP*0$u;}e~I0V{obtXdWr zYM5(#V7+s`l6TAbk&xh}n~a%jO?t;+OCz#7E24yCi8=2tmynwVI?jLQF(U>7IS}>uSoD4(@}aO ziRag|%XVD_aUQimNYfH{Y4>jgzaj`|t}<#6^Qt%JiRm@P>ke&+LRQ zy$)xO2L)=3FQ|m}mm>C@;bXQNJui0#phhpsk+|pCf@%4A@_<)!c`OL$0*PF80msrOJEz@DSS>jX)K_{g%>lLJSUvkP2 z?)#&XYh~_flq~W*M|0ZVB0GlD`N6Q`nGgW(+oMRHf&{OojonV@l8t^5Y@Sl$Lj-Wj zHV2b!3Jh9ok|OoBN;DY@nc1sy9Sd_jOaofaI_Ur&wZb{A5D8P`ID~=e;^A~mQ#`Bi zxxH|7D03lH>U1KnLHLjB{ssU^OKjb&qW|ErUZaro4e`T%vYH~MGiVb9wCUcKggPIZz-{`u$Yn=Q)g0~_QrD=j=>9f`O8qC z=)wApRw-_&2YqqEY<$oB0^9{+P@MakM($ zt9Yiw#}GRt&6a26Xufc7kGf6tuQh* zchXbN?02=p%vo+2i)WOr%xL7ZFYk+Dhn}{=Vu(V{+3iQAp{i z*YiRvH7)?kAF9CbVRBVTEIx2i2s%@_Y;p%kmwk6U_B}s;VbwDBwGKiA>0d9N-r3~z zrDTE7?qn+9hGdBXA#uC_Ut%FA7D#>yXgkN;TKH&YZ1}AT6>x+Wbzb252#E0+3rP+5 zwR-4g08oEUNb!yB|7VXOtZ>(AQW3u;d3J!5=`;%v66?zW_Gf6qWh%Ajd>TZ&-Z1V~ zvGVv8RGcnl71UMl=yvG?Bat0{m>cf@cpm!7QggWMC^nt<5|(Rp-Er95lB&`1Xaoel zqt+IYknlT80<1bUu#zUWoKSTB zSQqE>xUwsty!(d@QDrDdR*A0-#Y8CqY zK$%F~3Tk^7YlnRTpwbK`P%GrSmGWqnKnAv&-zx;Y`j%U>Gp+e==|5yhW~Mg_*&X zfc!NJcmkH&a(8$m2+69Q@;DnGX)IzraMp$6g(|bqWYaS^HKgZ!Fs&7SW~>aX9zez) zf3sU^oRDX~eOn)lMopKLE4s#xGIZ8GlEJG-4{rnmxoG*xCJcuFkX-_5x#Gc5#=fYq zk-LFbXmIahpAyhMa*Z?Q6Zjw;fI-@3CMfSoJ{)BWdAD*H&Nw;&#MFJMwJQT30Ez)- z)f9T~#r6;oLHfA>GirE5L;_;XwAT-aS%7&2@m0YGMlAs1eB3!(?-B@#CGPE8^ATM5 z=rp6LJ@87X0&z=+$pNJ)_d8l-;4DlEkDR$rYmh*#2y&6|*jQ))cX>C^&F2IFc$uGn_Q_X= zP(7dn-@!H0CZ(FC76tgoZ%OcMZ%&q}Sy+?|J=WnEd*VnjRK62v08l^BI^3JeTInU{ zA?6EUa5BK`I>rz-_!PD;;~5z-7Desey2ppp#d)ii?wN(W4>bg>G8KVf^FAP;a5cca z@c`m6t&%s8|NNx>MO8BV?#|njM8LC3v)Hw})&JL%N~a3QRvsTK5pnOY4rwFtSW^Hm z*#Lo%_X~2$gv<&~S=MRKV_9ZKM&Az?OeRf=K=-o<1ysPA__DENeg-n02I7gcbUX9le_pl0m*fBTdG!1f1;bQ2Dr)aS6g;UM0; z-U`zgsEx@7WEV>$0*sC4r-uT7$GC6gFdy9G)6Y+Y0zvsztA%dLh$zZxj|~g^IHDLM zP+J$h4InPnYEO9jrk{wI#O0Kgq1My9cNIrD0ltW%_W8~);TVJxXqfXrx|yf9oxx*Q z@?cQa2wYklE>S6aBh-LERE)boDiZesK;9KhO@E&hZi8qL$t?cJOsXW;jSjUrD(h3) z|kHMRg-h9hcJTK8cCt(bXhGh3FXQKTZq`>*Z(p!A-#4--Gu zrIDuNjrc2)y`39MGWmA9`DoV=h&SBsfMLI?0*X-hHyJLoA9QLgqFWzMS_HEE4D@Or z|GWy~tyi;p&8t}f`L^@b7Ou&7=gp@sukbc`B&BrJ_-52o)~esbXLxjyGUj#Q!8Zp2 z@@lII3ZcvzAe_~8bzku;WH|yHdPeKz=DL&LVqacCJ$;uE?gcyZ3dY%mwu(Xivck*f zhtosok;Km}e(Omp-$$T!<4b1{(kp^wC&wT1_J@rMc)|Mn8OSgAE1GyVP`YYcf`k9>su)pEBpx&X!b}WM9sf?2_4K4^J{u2! zPxzv3lx!OBXI%%1HR=FD~i;Xyqvl~zIsRrgeZ0vdrfxK z9I7^z$Rmz~7{B9klWg z#f}#r3SZBeHJktJe1j6Z+r`Zv8xT0sRx#|kH1=8{qW$gfaqq|q5Iwcyi53A5bu#dN z4FOP6U8yZonfm}IL=A6K#vB$$t0zaSHms{cVqwlrcTt>zt{0ZlHt26vW~iGPX&}Y( zNP@Fqa3Hhe1GoP&T>1y^RlqxC{N#Qztx$EA$|f3AC$-i%Fr9}7fJ)0I(v1WVuT(+1 zjzatEe#+Q>o60ttSEVWWD@;%X1_rsC%F+&yo_=lkS%I(n-C9w%$+-ZRjk4d0PP|dH zOU>1_Ryd}Y^kd|b!BXbmgvpY~D{5B5DeAr_F6&82Xr9R37Pf&b*#$5)Wm&tLVju>; zXP#_4tKa7Jhz068R9?QWwzpMWs?c8Wf)xd)1@{Ek>J=hXFj3L?0hTMKUc*OOS@1mQ zTcnfQYgGqfECz-B*Um=;z1tb1bh`%ddi_G zGZJj<8q!VIUG1mwaW3aASzjhNWHcj>w@aif?0q${!tMuiI5_a9>ob0HE@I<6&3t>e z9Y6+EJ{^FzQM(K(`+$oHF~O9RXavkrEY-D6=T-w&DG=gYo4%^B5MPj>WX8;IHMTk2 z*vBT4JM$0BxXDjEwK(cO&#W9tw}zbR6-MeH^*fnE{97ZITuWp#$h1NoQ|| zZHZ6gPXmH03|bC6ddGxrGEwo_{>n3ZoN0OwRj>G5j#LBCFrZjaMkBL$uIwcdH+H)= zqZ12g@FwACv{Gw@uyOccOnj`uwe=APe^(g(^$$RsRVzB4y%=X4g*mw*GdpBRAcnnN^w1Lga$hcO&Ak-&7ZH;2K zpbmNkTRh_^JRW}P97y{GVhLx{B{2|WXK@dPXO_4`P0Nc1WCdElSgLz}YtA+ z{n>T0P9bX|xB3mEcYpu7hOl##gc49mexWmJZ>?)slgM%dJT`&hC=p-#z(A(5Pm<=h z|E84d^kgM)DmbRDkSPErKk$kHd&$_8X#(G}`ykZVBui0OH~nx{&%uozvjwn3HeYGP za-mU~@R<)dI9nfueHxQDoUe`-p_)z3=BXDPWjKK%>#`_^?_I=^RKu+q=%p1f+Y|i;4O+hGcymG@$!(E; zG=^(#4b4r^+x`rf2tuHd!{FH&V(kFRS097l(t^k~-k5fGVwGT@W1;T@1&-r^Y>1xX zghQ6f-5%!W}Pu=`LAQ{Iz)sd_GGLYa!>Sjb1 zkyk11Z}ZK?3iA*kY~7%^kZ!bRi66LSXpoBH8Z4Dz;)6htsRhCv1qkF0Y1U^UhKV)_ z|JQ}Z!cz*zgJb)+0sWJ^UjxiWp~jf+&i#wA?714@3!&U4C5g$XDE4$Rh!lKs*>>=&Hq=au5xEB^ zQxQ|3v`Qq&B@@R5UQJrgcOOy}O6qW={IGCISW311i}hM{-th=O7v;CW!E|77OtUUk zSjvSnyM0UkyC(Q34&LqJC8d#93^t}G!lNVLM^6}3@9Yc+HlpQD?x`v3Mxml&5^Xnf zmi=g;31XNY*01kc(~mM{P67T;!iPgCg){|;d29 zX$tF2A}Ja6_;@J|aKmXp*|Y4IFJUoSCMaAS3FBOW=2r(VhH=0UPZlU#eTCQWGQQ=F zGJb)3tA4cXElJ{4)S#GAFRG?);C!G_B6vNAXc7ZsDrHNnM3idUvytz(u82Z}mEbAt zzs$_pC*kP$M^i~P4Wl&N^}0n*@1kyuQ7!1MW%fOsX~Rv`Jd_j1_OiHj>PjT+UL<;%a+78BPuF;QgTz=1k+ zOPeO?pl4 zxo4oVw3+7(A3j_Nsl0UQ7qV*AGTFKFPdRYlh?Fi}UY>n+q!#nRgU4men4$9Jmz(rC zd-fzqojUc^HLRO96j+R4{{bSp+OY8?2 z!JY@WztlSs3fA_js~SpdY;iep;+PU?<}rbR#D+@Lbx$lF$0jDaD= zqf3qvg0~&mO9-xkL*nvkY`;@ywaCP};Zesw`=+ zeDcY+^4xPXv~|X{CGX0W%gKrr8>C#hit2r4s0-@9Wy+M0AAa~l3HECBK3Q(Qxwi}$ z@UX7q7B5~>iMp-2p8##4Uw?g(a=ZU2RztzVSXhgkEG$sP#L`k<<=a9~*|o^xlG+0& z5Hap6?kyIWzcfyf2fB3gigg;sPDI=J2359yZH|ADcv1q)+g z&jZAT^}@Q*vYVY)fpm^xvju5eV9vv;A})8a0_p7MJomggj&n)3#R}w!`0e}2Nvwu~ zhY_s7;2?}(4F(5c1Zyxj2qRd7!9f_o8VnA?2-aY55Js>DgM%=Fbt8a{{|XZNLuI}+ zI3FWe3s;6q0)y0(&A)=Qsn5m`Mzg+7!N3enwD@}QVcD|wgt*OoD=;u(s>}UlrFgk8 zE5+bcj9?802Vq=dgTX-!U)!2a1chY27_Y|`9Gjp^~Z`iye(v!Apn97LBC!N2$_++aUIEtbhQXo z2pSGVKM_adgq)2?ssWLhtZz_{NchT#18G%pM8S4%+D;0giP9E?aD z)|hDGoVhpxQXC#8&SWGbJ|UiD9~lWEv^Agi^(Z7{9sZ)j20iX21L*NDEw&o)kPfR! z1nC;^-ALSH15O+9upal2Bbf(rc|2(YxxNFe-Bt-AAulsMO;Y@n*`O;;N~QQutD<<( z1=1}RRq~eSOS8q+I6kG0+P+h(NYcnk1SPwS25nBti@GRESfYPcovRnToRFrCEw}A( z`diX%ZJlg>T;nEX?!MBP?fniP3E~|rDKbMqkYXi=7{kc z7U69{OuRJJQr!I;=}9Sbfx{TO^z*FqyZm_ntx5loI$psA_V(%#oO#azEQw3zT^KDr165QH)PRE=g;mR{UkV=bVKCtzczgDgV~1RF~QQ}D{yoAkv#00rvd?4UgiCX zfxDS9@5E=bilwH(!arAUzIEpJ@Q<5^vx1MDe>1D_$G3_T^cS*Ta%Y=ZOEp_dvu=~C z*-aKutFn#TRJv`d97nd^q|P>JG?^;3Nu~PcR*?JE{u4BMYdv*s|1Y>a-Z4f5FK`K! z-a6JtH@m^-^C>+|hgw`T<5qf`>rXA_B@&X9BRM>5gBz-t7M8Ae%XiVQdtBb8qN=PA zA?#^)RI_&4Cuf?yb)E*doNZ&=au37009sGqS>7Nz?99lxxH~jO&Dnvk5G!3`i9%A+ z_Z(j$H~Y20TE60fLms+z^{2_H$>d34$ZtJUtb_&GGwgDquAf8zWwDt@3Tux10|SJ* A`2YX_ literal 0 HcmV?d00001 diff --git a/docs/source/_static/upstage-logo-small.png b/docs/source/_static/upstage-logo-small.png new file mode 100644 index 0000000000000000000000000000000000000000..4f7be6426729dc7574f668cfb06ac2310f5fc5a9 GIT binary patch literal 805 zcmZ`%T}YE*6h3RSZR%_?&1pHln##yrb928+Zno(+on~|Xb(3Fn)8Wt7{Ay_wLp01T zEW)r9%DPdD=#P*HLL3oJ`oV;PZZZP%B4k|@C7j-A&{Y@jdCqyx^PF=o-V3!g6{5tf zL;w)kX{(cIm@KEY%i(2O~)vOaiGXH|?wj=*t5L4*>ikRCpEOqzYiI z1;8{0kP(=8dZdIjrfMB78zMq60w9llG$0a9J%aYHGR_d5 z*hw_w2C);PW|P$6Z!(IMTVgY!ag!(H*0g*qCVDNR7GLPlt4Em@e-f_&zw0oh$1Xj# z>u@L07m*wB^?>0kxmnLTYwAF>jhClMm)SU+B>O&fGPl%`LfJ%0fg@91;wVp~s{}da zDaD55qU_S6XB*_^vW`G!`TiTXrf-uGoa0p$Rv1dirb!_T(9JyrC*zMZVr;G;37@rB z+xRO9LUH;YTjgMUZ?D~IaY>ilEmW#XJsG>!{3h-G!^zkOqajl|JDa+FeZ60C@SNh9 zxt5k%FAfb`c#%Qg+~a{9tF@A*v-Zm6`TVrYjbf*(U!aIC%uEGmgu>}VqNPg-!|jPe zp>OW)($&%Bx66_h=DcuqmozJLeC#^&Lk5kx{EuSU9W6N`m$fyqtqkiixq}`8pjH(c z3RKzxmC;qGGpP$r8cn`RZBnV8f0~bfn}3Ebf2+^i`+vi$XJ?)eLuPzKo!`rbn4kwj zp^(zo-O=M_0v@G5*f##HB#VgB<06-j?esu>poeAJJXASz%IEfXSJWSf5`", + "State": "{py:class}`~upstage_des.states.State`", + "Task": "{py:class}`~upstage_des.task.Task`", + "EnvironmentContext": "{py:class}`~upstage_des.base.EnvironmentContext`", + "UpstageBase": "{py:class}`~upstage_des.base.UpstageBase`", + "LinearChangingState": "{py:class}`~upstage_des.states.LinearChangingState`", + "DecisionTask": "{py:class}`~upstage_des.task.DecisionTask`", +} + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "pydata_sphinx_theme" +html_static_path = ["_static"] +html_logo = "_static/upstage-logo-medium.png" +html_show_sourcelink = False +html_theme_options = { + "show_nav_level": 2, + "navbar_center": ["navbar-nav"], + "logo": { + "text": "UPSTAGE", + }, + "show_toc_level": 1, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/gtri/upstage/", + "icon": "fa-brands fa-square-github", + "type": "fontawesome", + }, + ] +} diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 0000000..a5fd600 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,60 @@ +# ![logo](_static/upstage-logo-medium.png) UPSTAGE + +The Universal Platform for Simulating Tasks and Actors with Graphs and Events (UPSTAGE) library is a Python framework for creating robust, behavior-driven Discrete Event Simulations (DES). +The primary goal of UPSTAGE is to enable the quick creation of simulations at any desired level of abstraction with built-in data recording, simulation integrity and runtime checks, and +assistance for the usual pitfalls in custom discrete-event simulation: interrupts and cancellations. + +UPSTAGE - which is built on the [SimPy](https://simpy.readthedocs.io/en/latest/) library - contains two primary components that are assembled to create a broad array of simulations. + +The components are {{ Actor }} - which contain {{ State }} - and {{ Task }}. Actors can have multiple networks running on them, their states can be shared, and there are features for interactions between task networks running on the same actor. Those tasks modify the states on their actor, with features for real-time states that update on request without requiring time-stepping or modifying the existing events. + +```{image} _static/upstage-flow.png +:align: center +``` + +Additional features include: + +1. Context-aware {{ EnvironmentContext }}, accessed via {{ UpstageBase }}, enabling thread-safe simulation globals for the Stage and Named Entities (see below). +2. Active States, such as {{ LinearChangingState }} which represent continuous-time attributes of actors at discrete points. +3. Named Entitites in a thread-safe global context, enabling easier "director" logic creation with fewer args in your code +4. The stage: a global context variable for simulation properties and attributes. This enables under-the-hood coordination of motion, geography, and other features. +5. All States are recordable, and some record dataclass and dictionary values. +6. Numerous runtime checks and error handling for typical DES pitfalls: based on years of custom DES-building experience. +7. And more! + +```{note} +This project is under active development. This branch of the docs is for the unreleased version 1.0. The docs may be innacurate. +``` + +## Installation from source + +You can download UPSTAGE and install it manually. Clone, or download the archive and extract it. From the extraction location (and within a suitable Python environment): + +```console +(venv) $ python -m pip install . +``` + +(or just `pip install .`) + +## Contributing + +To contribute to UPSTAGE, or to learn the steps for building documentation, running tests, and putting +in PRs, see [CONTRIBUTING.MD](https://github.com/gtri/upstage/blob/main/CONTRIBUTING.md)) + +## License and Attribution + +This software is licensed under the BSD 3-Clause. Please see the `LICENSE` file in the repository for details. + +## Reference + +This section contains information-oriented reference materials for developers +looking to understand the UPSTAGE software components and its API. + +The API documentation is auto-generated. + +```{toctree} +:caption: API +:maxdepth: 2 + +auto/modules.rst +``` diff --git a/src/upstage_des/__init__.py b/src/upstage_des/__init__.py index 898b6d1..f9e0452 100644 --- a/src/upstage_des/__init__.py +++ b/src/upstage_des/__init__.py @@ -1,70 +1,19 @@ -"""upstage_des: Discrete event simulation library built on SimPy.""" +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) -from upstage_des.actor import EMPTY_KNOWLEDGE, Actor -from upstage_des.base import ( - ENTITY_REGISTRY_CONTEXT_VAR, - ENV_CONTEXT_VAR, - STAGE_CONTEXT_VAR, - EnvironmentContext, - SimulationError, - Stage, - UpstageBase, - UpstageError, - add_stage_variable, - clear_top_context, - create_top_context, - get_entities_by_class, - get_entity_registry, - get_stage, - get_stage_variable, -) -from upstage_des.events import ( - Any, - Event, - FilterGet, - Get, - Put, - ResourceHold, - Wait, -) -from upstage_des.states import ( - LinearChangingState, - State, -) -from upstage_des.tasks import ( - DecisionTask, - InterruptStates, - Task, -) +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. -__all__ = [ - "Actor", - "EMPTY_KNOWLEDGE", - "ENTITY_REGISTRY_CONTEXT_VAR", - "ENV_CONTEXT_VAR", - "STAGE_CONTEXT_VAR", - "EnvironmentContext", - "SimulationError", - "Stage", - "UpstageBase", - "UpstageError", - "add_stage_variable", - "clear_top_context", - "create_top_context", - "get_entities_by_class", - "get_entity_registry", - "get_stage", - "get_stage_variable", - "Any", - "Event", - "FilterGet", - "Get", - "Put", - "ResourceHold", - "Wait", - "LinearChangingState", - "State", - "DecisionTask", - "InterruptStates", - "Task", -] +"""A framework for modeling and simulating complex systems of systems. + +UPSTAGE (i.e., the Universal Platform for Simulating Tasks and Actors with +Graphs and Events) is built atop of SimPy, with the intent of simplifying the +development process for complex simulations. + +""" + +from ._logging import _install_null_handler +from ._version import __authors__, __version__ + +_install_null_handler() + +__all__ = ("__authors__", "__version__") diff --git a/src/upstage_des/_version.py b/src/upstage_des/_version.py new file mode 100644 index 0000000..70e1f44 --- /dev/null +++ b/src/upstage_des/_version.py @@ -0,0 +1,9 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Declare the UPSTAGE version.""" + +__authors__ = "UPSTAGE Contributors, GTRI" +__version__ = "1.0.0" From cad036cca953e4719d34f8f6be28a5fec6d1008f Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 18:20:04 -0400 Subject: [PATCH 05/12] Applying logger changes from #115 thanks to @sanbales --- docs/source/features/logging.md | 42 ++++++++++++ src/upstage_des/_logging.py | 47 +++++++++++++ src/upstage_des/actor.py | 26 +++++-- src/upstage_des/api.py | 79 +++++++++++++++++++++ src/upstage_des/rehearsing.py | 8 --- tests/test_actor_clone.py | 6 +- tests/test_events.py | 2 +- tests/test_logging.py | 117 ++++++++++++++++++++++++++++++++ 8 files changed, 311 insertions(+), 16 deletions(-) create mode 100644 docs/source/features/logging.md create mode 100644 src/upstage_des/_logging.py create mode 100644 src/upstage_des/api.py delete mode 100644 src/upstage_des/rehearsing.py create mode 100644 tests/test_logging.py diff --git a/docs/source/features/logging.md b/docs/source/features/logging.md new file mode 100644 index 0000000..bd9f7f7 --- /dev/null +++ b/docs/source/features/logging.md @@ -0,0 +1,42 @@ +# Logging + +UPSTAGE exposes a :mod:`logging` hierarchy rooted at ``upstage_des``. The +library is silent by default — a ``NullHandler`` is attached on import and +the package logger defaults to ``WARNING``. Opt in at your application +entry point + +```{python} + import logging + + logging.basicConfig(level=logging.INFO, format="%(name)s: %(message)s") + logging.getLogger("upstage_des").setLevel(logging.INFO) +``` + +Actor events go to ``upstage_des.actor.``. + +```{python} + logging.getLogger("upstage_des.actor").setLevel(logging.INFO) + # Filter only for actors with certain values in the name + logging.getLogger("upstage_des.actor").addFilter( + lambda rec: "Plane" not in rec.name + ) +``` + +Inside a custom :class:`Task`, call ``actor.write_to_log`` with printf-style +arguments. Formatting is deferred — when the log level is disabled and +``debug_logging=False`` on the actor, the interpolation never runs, so +``repr``/``str`` cost on your arguments is avoided in hot loops. + +```{python} + def task(self, *, actor): + actor.log("picked up %s (qty=%d)", item, qty) # INFO + actor.log("low fuel: %.1f%%", remaining, level=logging.WARNING) +``` + +Two independent sinks are driven by every ``write_to_log`` call: + +* The per-actor in-memory list (``actor.write_to_log()`` / + ``actor.get_log()``) — controlled by the ``debug_loging`` flag set at + actor construction. Use this for post-run analysis in notebooks. +* Python's ``logging`` — controlled by the standard level hierarchy. + Use this for structured sinks (files, JSON, stdout during dev). diff --git a/src/upstage_des/_logging.py b/src/upstage_des/_logging.py new file mode 100644 index 0000000..6c1f0fa --- /dev/null +++ b/src/upstage_des/_logging.py @@ -0,0 +1,47 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Logger helpers for actors. + +UPSTAGE exposes a logger hierarchy rooted at ``upstage_des``. Individual +actors emit through ``upstage_des.actor.`` so users can filter, +silence, or route per-actor output with standard ``logging`` configuration. + +By default, the root ``upstage_des`` logger has a ``NullHandler`` attached +and inherits ``logging.WARNING`` — quiet by default. Users opt in by +configuring the logger at their application entry point:: + + import logging + logging.getLogger("upstage_des").setLevel(logging.INFO) + logging.basicConfig() +""" + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from upstage_des.actor import Actor # pragma: no cover + +ROOT_LOGGER_NAME = "upstage_des" +ACTOR_LOGGER_PREFIX = f"{ROOT_LOGGER_NAME}.actor" + + +def _install_null_handler() -> None: + """Attach a ``NullHandler`` to the package logger so library use is silent by default.""" + root = logging.getLogger(ROOT_LOGGER_NAME) + if not any(isinstance(h, logging.NullHandler) for h in root.handlers): + root.addHandler(logging.NullHandler()) + if root.level == logging.NOTSET: + root.setLevel(logging.WARNING) + + +def get_actor_logger(actor: "Actor") -> logging.Logger: + """Return the ``logging.Logger`` for a given actor. + + Rehearsal clones get a ``.rehearsal`` suffix so their emissions can + be filtered independently from real-run logs. + """ + name = f"{ACTOR_LOGGER_PREFIX}.{actor.name}" + return logging.getLogger(name) diff --git a/src/upstage_des/actor.py b/src/upstage_des/actor.py index e449141..4c9627a 100644 --- a/src/upstage_des/actor.py +++ b/src/upstage_des/actor.py @@ -5,6 +5,7 @@ """Actor system with dataclass-like field transformation and State descriptors.""" +import logging from collections import OrderedDict, defaultdict, deque from collections.abc import Iterable from copy import deepcopy @@ -13,6 +14,7 @@ from simpy import Process +from upstage_des._logging import get_actor_logger from upstage_des.base import ( SimulationError, UpstageBase, @@ -93,6 +95,9 @@ def __init__(self: Any, **kwargs: Any) -> None: if hasattr(cls, "__post_init__"): cls.__post_init__(self) + # logging need `name`, so it comes later. + self._logger = get_actor_logger(self) + cls.__init__ = __init__ @@ -124,6 +129,7 @@ class _BaseActor(UpstageBase): """ name: str + debug_logging: bool knowledge: dict[str, Any] _is_clone: bool @@ -133,6 +139,7 @@ class _BaseActor(UpstageBase): __model_fields__: dict[str, State] _states_by_cause: dict[Any, set[str]] _causes_by_state: dict[str, Any] + _logger: logging.Logger def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -155,13 +162,24 @@ def _record_state_change(self, name: str, value: Any, extra: Any | None = None) ########################################################### ### Logging ############################################## - def write_to_log(self, to_write: str) -> None: + def write_to_log(self, to_write: str, *args: Any, level: int = logging.INFO) -> None: """Write to the log. Args: to_write (str): The text to write + args (Any): Objects to formate into the `to_write` string. + level (int): Logging library level. Defaults to INFO. """ - self._log.append((self.env.now, to_write)) + logger = self._logger + logger_wants = logger.isEnabledFor(level) + + if not self.debug_logging and not logger_wants: + return None + formatted = to_write % args if args else to_write + if self.debug_logging: + self._log.append((self.env.now, formatted)) + if logger_wants: + logger.log(level, formatted) def get_log(self) -> deque[tuple[float, str]]: """Retrieve the log. @@ -393,14 +411,14 @@ def clone(self) -> Self: Returns: Self: A cloned actor with the same state values """ - kwargs = {} + kwargs: dict[str, Any] = {} for field_name, field_obj in self.__model_fields__.items(): current_value = getattr(self, field_name) if isinstance(current_value, Actor): kwargs[field_name] = current_value else: kwargs[field_name] = deepcopy(current_value) - + kwargs["name"] = kwargs["name"] + ".clone" cloned = type(self)(**kwargs) cloned._state_histories = {} cloned._is_clone = True diff --git a/src/upstage_des/api.py b/src/upstage_des/api.py new file mode 100644 index 0000000..43ef863 --- /dev/null +++ b/src/upstage_des/api.py @@ -0,0 +1,79 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""API for standard usage of upstage-des.""" + +from upstage_des.actor import EMPTY_KNOWLEDGE, Actor +from upstage_des.base import ( + ENTITY_REGISTRY_CONTEXT_VAR, + ENV_CONTEXT_VAR, + SIMPY_GEN, + STAGE_CONTEXT_VAR, + EnvironmentContext, + SimulationError, + Stage, + UpstageBase, + UpstageError, + add_stage_variable, + clear_top_context, + create_top_context, + get_entities_by_class, + get_entity_registry, + get_stage, + get_stage_variable, +) +from upstage_des.events import ( + Any, + Event, + FilterGet, + Get, + Put, + ResourceHold, + Wait, +) +from upstage_des.states import ( + LinearChangingState, + State, +) +from upstage_des.tasks import ( + TASK_GEN, + DecisionTask, + InterruptStates, + Task, +) + +__all__ = [ + "Actor", + "EMPTY_KNOWLEDGE", + "ENTITY_REGISTRY_CONTEXT_VAR", + "ENV_CONTEXT_VAR", + "STAGE_CONTEXT_VAR", + "EnvironmentContext", + "SimulationError", + "Stage", + "UpstageBase", + "UpstageError", + "add_stage_variable", + "clear_top_context", + "create_top_context", + "get_entities_by_class", + "get_entity_registry", + "get_stage", + "get_stage_variable", + "Any", + "Event", + "FilterGet", + "Get", + "Put", + "ResourceHold", + "Wait", + "LinearChangingState", + "State", + "DecisionTask", + "InterruptStates", + "Task", + "TASK_GEN", + "SIMPY_GEN", +] diff --git a/src/upstage_des/rehearsing.py b/src/upstage_des/rehearsing.py deleted file mode 100644 index 4ef790b..0000000 --- a/src/upstage_des/rehearsing.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Helpers for doing rehearsal.""" - -... diff --git a/tests/test_actor_clone.py b/tests/test_actor_clone.py index 1806688..7fece63 100644 --- a/tests/test_actor_clone.py +++ b/tests/test_actor_clone.py @@ -5,7 +5,7 @@ """Test cloning actors.""" -from upstage_des import Actor, EnvironmentContext, get_entities_by_class +from upstage_des.api import Actor, EnvironmentContext, get_entities_by_class def test_actor_clone_basic() -> None: @@ -18,7 +18,7 @@ class Vehicle(Actor): cloned = vehicle.clone() - assert cloned.name == "car1" + assert cloned.name == "car1.clone" assert cloned.fuel == 50.0 assert cloned.position == 10 assert cloned.is_clone is True @@ -97,7 +97,7 @@ class Car(Vehicle): cloned = car.clone() - assert cloned.name == "sedan" + assert cloned.name == "sedan.clone" assert cloned.fuel == 75.0 assert cloned.passengers == 3 assert cloned.is_clone is True diff --git a/tests/test_events.py b/tests/test_events.py index 14478ea..eadaa78 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -11,7 +11,7 @@ from simpy.resources.container import ContainerGet, ContainerPut from simpy.resources.store import StoreGet, StorePut -from upstage_des import ( +from upstage_des.api import ( EnvironmentContext, SimulationError, ) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..1cb1fc3 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,117 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +from collections import deque +import logging + +import pytest + +import upstage_des.api as UP + + +class Bot(UP.Actor): + """Minimal actor for logging tests.""" + + +def test_logs_property_returns_list() -> None: + """`actor.get_log()` returns the in-memory list.""" + with UP.EnvironmentContext(): + bot = Bot(name="b") + assert bot.get_log() == deque() + bot.write_to_log("hello") + assert len(bot.get_log()) == 1 + assert "hello" in bot.get_log()[0][1] + + +def test_get_log_still_works() -> None: + """`actor.get_log()` continues to return the in-memory list.""" + with UP.EnvironmentContext(): + bot = Bot(name="b") + bot.write_to_log("one") + assert bot.get_log() is bot.get_log() + assert len(bot.get_log()) == 1 + + +def test_log_list_still_populates() -> None: + """Back-compat: the _log list still receives entries.""" + with UP.EnvironmentContext(): + bot = Bot(name="b") + bot.write_to_log("first") + bot.write_to_log("second") + assert len(bot._log) == 2 + assert "first" in bot._log[0][1] + assert "second" in bot._log[1][1] + + +def test_log_false_suppresses_list() -> None: + """`debug_logging=False` stops the in-memory list from growing.""" + with UP.EnvironmentContext(): + bot = Bot(name="b", debug_logging=False) + bot.write_to_log("silent") + assert bot._log == deque() + + +def test_logging_module_receives_records(caplog: pytest.LogCaptureFixture) -> None: + """Records reach the `upstage_des.actor.` logger.""" + with caplog.at_level(logging.INFO, logger="upstage_des"): + with UP.EnvironmentContext(): + bot = Bot(name="alice") + bot.write_to_log("hi world") + records = [r for r in caplog.records if r.name.startswith("upstage_des.actor.alice")] + assert len(records) == 1 + assert records[0].getMessage() == "hi world" + assert records[0].levelno == logging.INFO + + +def test_level_argument(caplog: pytest.LogCaptureFixture) -> None: + """Custom `level=` routes to the matching logging level.""" + with caplog.at_level(logging.DEBUG, logger="upstage_des"): + with UP.EnvironmentContext(): + bot = Bot(name="b") + bot.write_to_log("verbose", level=logging.DEBUG) + bot.write_to_log("normal") + bot.write_to_log("alarm", level=logging.WARNING) + levels = {r.getMessage(): r.levelno for r in caplog.records if r.name.startswith("upstage_des")} + assert levels["verbose"] == logging.DEBUG + assert levels["normal"] == logging.INFO + assert levels["alarm"] == logging.WARNING + + +def test_level_filter_short_circuits_logger() -> None: + """Records below the logger's level are dropped before emission.""" + seen: list[logging.LogRecord] = [] + + class Capture(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + seen.append(record) + + handler = Capture(level=logging.DEBUG) + root = logging.getLogger("upstage_des") + prior = root.level + root.addHandler(handler) + try: + root.setLevel(logging.WARNING) + with UP.EnvironmentContext(): + bot = Bot(name="quiet", debug_logging=False) + bot.write_to_log("ignored") # INFO default, below WARNING + assert not seen + finally: + root.removeHandler(handler) + root.setLevel(prior) + + +def test_package_logger_has_null_handler() -> None: + """Importing the package installs a NullHandler so library use is silent.""" + root = logging.getLogger("upstage_des") + assert any(isinstance(h, logging.NullHandler) for h in root.handlers) + + +def test_printf_style_formatting() -> None: + """printf-style args are interpolated when a sink wants the record.""" + with UP.EnvironmentContext(): + bot = Bot(name="b") + bot.write_to_log("count=%d name=%s", 3, "foo") + entry = bot._log[-1][1] + assert "count=3 name=foo" in entry From 983e688e99e177d2eec096a62cb57fb7765d093c Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 18:24:41 -0400 Subject: [PATCH 06/12] Logging docs --- docs/source/features/logging.md | 6 +++--- docs/source/index.md | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/source/features/logging.md b/docs/source/features/logging.md index bd9f7f7..6fcd4d1 100644 --- a/docs/source/features/logging.md +++ b/docs/source/features/logging.md @@ -5,7 +5,7 @@ library is silent by default — a ``NullHandler`` is attached on import and the package logger defaults to ``WARNING``. Opt in at your application entry point -```{python} +```{code-block} python import logging logging.basicConfig(level=logging.INFO, format="%(name)s: %(message)s") @@ -14,7 +14,7 @@ entry point Actor events go to ``upstage_des.actor.``. -```{python} +```{code-block} python logging.getLogger("upstage_des.actor").setLevel(logging.INFO) # Filter only for actors with certain values in the name logging.getLogger("upstage_des.actor").addFilter( @@ -27,7 +27,7 @@ arguments. Formatting is deferred — when the log level is disabled and ``debug_logging=False`` on the actor, the interpolation never runs, so ``repr``/``str`` cost on your arguments is avoided in hot loops. -```{python} +```{code-block} python def task(self, *, actor): actor.log("picked up %s (qty=%d)", item, qty) # INFO actor.log("low fuel: %.1f%%", remaining, level=logging.WARNING) diff --git a/docs/source/index.md b/docs/source/index.md index a5fd600..5391749 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -36,6 +36,16 @@ You can download UPSTAGE and install it manually. Clone, or download the archive (or just `pip install .`) +## Features + +```{toctree} +:caption: Features +:maxdepth: 2 + +features/logging.md +``` + + ## Contributing To contribute to UPSTAGE, or to learn the steps for building documentation, running tests, and putting From 84a6cd1027e10a221f7d2e833cacf067666f54b1 Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 18:36:35 -0400 Subject: [PATCH 07/12] Removing explicit python 3.11 support. --- .github/workflows/lint.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5c27b82..0ebe9f6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - environment: [py311, py312, py313] + environment: [py312, py313, py314] steps: - uses: actions/checkout@v6 - uses: prefix-dev/setup-pixi@v0.9.5 diff --git a/pyproject.toml b/pyproject.toml index 50cc851..020f5f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", ] From ae28e854ef0f0b29629d91af9f61a62a1daf100a Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 18:39:14 -0400 Subject: [PATCH 08/12] Updated lock --- pixi.lock | 8281 ++++++++++++++++++++++------------------------------- 1 file changed, 3431 insertions(+), 4850 deletions(-) diff --git a/pixi.lock b/pixi.lock index 920a461..c57eb41 100644 --- a/pixi.lock +++ b/pixi.lock @@ -7,83 +7,89 @@ environments: - https://pypi.org/simple packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-64: - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hfdf4475_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.2-h534c281_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.4-h7c6738f_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.2-h81fe080_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_25.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ dev: @@ -93,755 +99,750 @@ environments: - https://pypi.org/simple packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py312h66e93f0_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py312h178313f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py312h7900ff3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.7-h0d44e9d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py312he28fd5a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py313h4a16004_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py312h66e93f0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.5.1-py39h77e2912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.21.1-py310hd8a072f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.4.0-py312hbf22597_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.25.1-py312h680f630_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py312hf79aa60_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.11-h7805a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-21.2.0-py312hb553811_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312h5861a67_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py313hf050af9_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.4.0-py313h116f8bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py313h8d69aa9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py312hf857d28_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py313hf57695f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py312h3520af0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.14-py312haafddd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py313h035b7d0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.20-py313h8b5a893_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/jsonpointer-3.0.0-py312hb401068_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-20.1.1-hf95d169_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h207b36a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h4b5e92a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.7-hebb159f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.39-h03b04e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py312h91b2f42_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py312h3520af0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.21-hc6ced15_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h77d7759_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h0d7f165_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h0712280_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py313hdc5d0a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py313h035b7d0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py312h01d7ebd_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py313hf59fe81_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py312h01d7ebd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py313h16366db_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-11.0-py312h2365019_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-11.0-py312h2365019_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.5.1-py39h286ba15_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-12.1-py313h07bcf3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-12.1-py313hf669bc3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.21.1-py310hb9b2626_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.9-h9ccd52b_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312h3520af0_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-26.4.0-py312h679dbab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.13-h3d5d122_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py313h22ab4a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py313h7c6a591_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312h2ac7433_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.25.1-py312haba3716_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py312ha54e1fc_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.30.0-py313hcc225dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.11-h16586dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.1-py312h01d7ebd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py313hf59fe81_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h7130eaa_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py312h01d7ebd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h27d9b8f_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-21.2.0-py313h20a7fcf_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313h3579c5c_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py313hc845a76_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py313ha9b7d5b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py313h928ef07_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py314h6e9b3f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.1-ha82da77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.7-h178c5d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.39-h223e5b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py313hbcba52a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py313ha9b7d5b_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py314h264e108_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py313h90d716c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py314hbdd0d06_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py313h90d716c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.0-py313hb6afeec_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.0-py313hb6afeec_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py313hf8519d8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.21.1-py310h3b8a9b8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.2-h81fe080_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py313ha9b7d5b_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-26.4.0-py313he6960b1_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py314ha14b1ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.25.1-py313hf3ab51e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py313h35210b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.11-hc5c3a1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py313h90d716c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py313h90d716c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py313ha7868ed_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313h5813708_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py313hb4c8b1a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.2-py313hd8ed1ab_101.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py313h5813708_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py314h2359020_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.7-he286e8c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py313hd92aa1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py313hb4c8b1a_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py314hcdb55d9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py314h5a2d7ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py313ha7868ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.5.1-py39he870945_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.21.1-py310ha413424_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.2-h4df99d1_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py313h5813708_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313hb4c8b1a_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-26.4.0-py313h2100fd5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py314hc5dbbe4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.25.1-py313ha8a9a3c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py313he8c32b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.11-h02f8532_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py313ha7868ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.42.34438-h7142326_25.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313ha7868ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ py312: @@ -851,753 +852,750 @@ environments: - https://pypi.org/simple packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py312h66e93f0_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py312h460c074_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py312h178313f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py312h8a5da7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py312h8285ef7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py312h7900ff3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.7-h0d44e9d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py312he28fd5a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py312h66e93f0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py312h4c3975b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.5.1-py39h77e2912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.21.1-py310hd8a072f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.4.0-py312hbf22597_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py312h5253ce2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.25.1-py312h680f630_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py312hf79aa60_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py312h868fb18_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.11-h7805a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-21.2.0-py312hb553811_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312h5861a67_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py312h80b0991_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.4.0-py312h5f4ecc6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py312hf857d28_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py312he90777b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py312h3520af0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.14-py312haafddd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py312heb39f77_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.20-py312h29de90a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/jsonpointer-3.0.0-py312hb401068_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-20.1.1-hf95d169_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h207b36a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h4b5e92a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.7-hebb159f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.39-h03b04e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py312h91b2f42_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py312h3520af0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.21-hc6ced15_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py312heb39f77_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py312h01d7ebd_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py312h933eb07_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py312h01d7ebd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-11.0-py312h2365019_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-11.0-py312h2365019_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.5.1-py39h286ba15_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-12.1-py312h4a480f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-12.1-py312h1993040_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.21.1-py310hb9b2626_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.9-h9ccd52b_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312h3520af0_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-26.4.0-py312h679dbab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.13-ha9537fe_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py312hba6025d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312h2ac7433_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.25.1-py312haba3716_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py312ha54e1fc_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.30.0-py312h8a6388b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.11-h16586dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.1-py312h01d7ebd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h7130eaa_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py312h01d7ebd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h27d9b8f_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-21.2.0-py312h024a12e_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hde4cb15_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py312h998013c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py312hd8f9ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py312h04c11ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py312h6510ced_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py312h81bd7bf_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.1-ha82da77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.7-h178c5d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.39-h223e5b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py312h9535dd2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py312hea69d52_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py312hefc2c51_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.0-py312hb9d441b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.0-py312hb9d441b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py312h6f6235b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py312h19bbe71_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py312h1de3e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.21.1-py310h3b8a9b8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.9-hc22306f_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-26.4.0-py312hf4875e0_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py312hb3ab3e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.25.1-py312hd3c0895_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py312h31a5b27_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py312h6ef9ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.11-hc5c3a1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312hea69d52_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py312h4389bb4_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py312h06d0912_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py312h4389bb4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py312h31fea79_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py312h275cf98_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py312h05f76fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py312ha1a9051_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py312h2e8e312_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-pyodide-kernel-0.6.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.7-he286e8c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py312h53bce91_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py312h2f35c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py312h4389bb4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py312he06e257_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.5.1-py39he870945_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.21.1-py310ha413424_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.9-h3f84c4b_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py312h275cf98_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-26.4.0-py312hd7027bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py312he5662c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py312h829343e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.25.1-py312h8422cdd_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py312hc33538c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py312hdabe01f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.11-h02f8532_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.42.34438-h7142326_25.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py312h4389bb4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ py313: @@ -1607,375 +1605,372 @@ environments: - https://pypi.org/simple packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h46c70d0_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py313h8060acc_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.2-py313hd8ed1ab_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.7-h0d44e9d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py313h6eb7059_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py313h4a16004_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py313h536fd9c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py313h536fd9c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.5.1-py39h77e2912_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.21.1-py310hd8a072f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.2-h4df99d1_101.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py313hfe82de2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.11-h7805a7d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h536fd9c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py313h9ea2907_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py313h49682b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.4.0-py313h116f8bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py313h8d69aa9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py313h717bdf5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.2-py313hd8ed1ab_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py313h035b7d0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-20.1.1-hf95d169_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h4b5e92a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hfdf4475_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.7-hebb159f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.39-h03b04e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py313he540dec_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py313h717bdf5_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py313hdc5d0a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py313h035b7d0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py313h63b0ddb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py313h63b0ddb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.5.1-py39h286ba15_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py313hf59fe81_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py313h16366db_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.21.1-py310hb9b2626_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.2-h534c281_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.2-h4df99d1_101.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py313h717bdf5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py313h2e013d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.13-h3d5d122_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py313h22ab4a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py313h7c6a591_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.11-h16586dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py313h63b0ddb_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313h3579c5c_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py313hc845a76_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py313h7208f8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py313ha9b7d5b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py313h65a2061_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.1-ha82da77_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.7-h178c5d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.39-h223e5b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py313hbcba52a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py313ha9b7d5b_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h6967ea9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py313h28ec6f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py313h90d716c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py313h90d716c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py313hf8519d8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py313hd3e6d80_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py313h6688731_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.21.1-py310h3b8a9b8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.2-h81fe080_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py313ha9b7d5b_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py313h35210b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py313h6688731_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h65a2061_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.11-hc5c3a1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py313h90d716c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313h5813708_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py313h2a31948_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py313hb4c8b1a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.2-py313hd8ed1ab_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.7-he286e8c_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py313hd92aa1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py313hb4c8b1a_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py313h1af1686_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.5.1-py39he870945_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.21.1-py310ha413424_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-json-report-1.5.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.2-h4df99d1_101.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313hb4c8b1a_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py313he8c32b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.11-h02f8532_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_25.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.42.34438-h7142326_25.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313ha7868ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ py314: @@ -2354,13 +2349,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl - pypi: ./ packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 @@ -2373,33 +2361,8 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] - size: 28948 - timestamp: 1770939786096 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge - - libgomp >=7.5.0 - constrains: - - openmp_impl 9999 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 23621 - timestamp: 1650670423406 -- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_1.conda - sha256: aeee03ce021e13648c82414358616cc3edad15101ef354cae9a2d4ba3ba7a5e4 - md5: 72bdca5fa72b5b89fc8a86d2e98793f0 - depends: - - cpython - - python-gil - license: MIT - license_family: MIT - purls: [] - size: 8283 - timestamp: 1736938720099 + size: 28948 + timestamp: 1770939786096 - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 md5: aaa2a381ccc56eac91d63b6c1240312f @@ -2434,25 +2397,25 @@ packages: - pkg:pypi/alabaster?source=hash-mapping size: 18684 timestamp: 1733750512696 -- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda - sha256: b28e0f78bb0c7962630001e63af25a89224ff504e135a02e50d4d80b6155d386 - md5: 9749a2c77a7c40d432ea0927662d7e52 +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e + md5: af2df4b9108808da3dc76710fe50eae2 depends: - exceptiongroup >=1.0.2 - idna >=2.8 - - python >=3.9 - - sniffio >=1.1 + - python >=3.10 - typing_extensions >=4.5 - python constrains: - - trio >=0.26.1 - - uvloop >=0.21 + - trio >=0.32.0 + - uvloop >=0.22.1 + - winloop >=0.2.3 license: MIT license_family: MIT purls: - pkg:pypi/anyio?source=hash-mapping - size: 126346 - timestamp: 1742243108743 + size: 146764 + timestamp: 1774359453364 - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf md5: 54898d0f524c9dee622d44bbb081a8ab @@ -2476,27 +2439,42 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/argon2-cffi?source=compressed-mapping + - pkg:pypi/argon2-cffi?source=hash-mapping size: 18715 timestamp: 1749017288144 -- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py312h66e93f0_5.conda - sha256: 3cbc3b026f5c3f26de696ead10607db8d80cbb003d87669ac3b02e884f711978 - md5: 1505fc57c305c0a3174ea7aae0a0db25 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda + sha256: 7988c207b2b766dad5ebabf25a92b8d75cb8faed92f256fd7a4e0875c9ec6d58 + md5: 1567f06d717246abab170736af8bad1b depends: - __glibc >=2.17,<3.0.a0 - cffi >=1.0.1 - - libgcc >=13 + - libgcc >=14 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 34847 - timestamp: 1725356749774 -- conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-21.2.0-py312hb553811_5.conda - sha256: 37d61df3778b99e12d8adbaf7f1c5e8b07616ef3ada4436ad995f25c25ae6fda - md5: 033345df1d545bc40b52e03cb03db4e0 + size: 35646 + timestamp: 1762509443854 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + sha256: ad188ccc06a06c633dc124b09e9e06fb9df4c32ffc38acc96ecc86e506062090 + md5: 27bbec9f2f3a15d32b60ec5734f5b41c + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.0.1 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 35943 + timestamp: 1762509452935 +- conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py312h80b0991_2.conda + sha256: b18ea88c1a3e8c9d6a05f1aa71928856cfdcb5fd4ad0353638f4bac3f0b9b9a2 + md5: 66f6b81d4bf42e3da028763e9d873bff depends: - __osx >=10.13 - cffi >=1.0.1 @@ -2506,11 +2484,25 @@ packages: license_family: MIT purls: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 31898 - timestamp: 1725356938246 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-21.2.0-py312h024a12e_5.conda - sha256: 0e32ddd41f273f505956254d81ffadaf982ed1cb7dfd70d9251a8c5b705c7267 - md5: 6ccaeafe1a52b0d0e7ebfbf53a374649 + size: 33431 + timestamp: 1762509769660 +- conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py313hf050af9_2.conda + sha256: e2644e87c26512e38c63ace8fc19120a472c0983718a8aa264862c25294d0632 + md5: 1fedb53ffc72b7d1162daa934ad7996b + depends: + - __osx >=10.13 + - cffi >=1.0.1 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 33301 + timestamp: 1762509795647 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda + sha256: 24c475f6f7abf03ef3cc2ac572b7a6d713bede00ef984591be92cdc439b09fbc + md5: 0a2a07b42db3f92b8dccf0f60b5ebee8 depends: - __osx >=11.0 - cffi >=1.0.1 @@ -2521,117 +2513,107 @@ packages: license_family: MIT purls: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 32838 - timestamp: 1725356954187 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-21.2.0-py313h20a7fcf_5.conda - sha256: 2ced37cabe03f64f2ecc36a089576b79b27f3f2d4beefceb0d614bf40450d53a - md5: ba06ad3e96ea794fec0eddfa92e121b5 + size: 34224 + timestamp: 1762509989973 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py314h0612a62_2.conda + sha256: aab60bbaea5cc49dff37438d1ad469d64025cda2ce58103cf68da61701ed2075 + md5: a240a79a49a95b388ef81ccda27a5e51 depends: - __osx >=11.0 - - cffi >=1.0.1 - - python >=3.13.0rc1,<3.14.0a0 - - python >=3.13.0rc1,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - cffi >=2.0.0b1 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT purls: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 32946 - timestamp: 1725356801521 -- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py312h4389bb4_5.conda - sha256: 8764a8a9416d90264c7d36526de77240a454d0ee140841db545bdd5825ebd6f1 - md5: 53943e7ecba6b3e3744b292dc3fb4ae2 + size: 34218 + timestamp: 1762509977830 +- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda + sha256: 38c5e43d991b0c43713fa2ceba3063afa4ccad2dd4c8eb720143de54d461a338 + md5: 5dc3781bbc4ddce0bf250a04c1a192c2 depends: - cffi >=1.0.1 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 34399 - timestamp: 1725357069475 -- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py313ha7868ed_5.conda - sha256: 36b79f862177b3a104762f68664e445615e7c831ca5fe76dc4596ad531ed46a3 - md5: 6d6dbb065c660e9e358b32bdab9ada31 + size: 38535 + timestamp: 1762509763237 +- conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py314h5a2d7ad_2.conda + sha256: a742e7cd0d5534bfff3fd550a0c1e430411fad60a24f88930d261056ab08096f + md5: ffa247e46f47e157851dc547f4c513e4 depends: - - cffi >=1.0.1 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - cffi >=2.0.0b1 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 34467 - timestamp: 1725357154522 -- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - sha256: c4b0bdb3d5dee50b60db92f99da3e4c524d5240aafc0a5fcc15e45ae2d1a3cd1 - md5: 46b53236fdd990271b03c3978d4218a9 + size: 38653 + timestamp: 1762509771011 +- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 + md5: 85c4f19f377424eafc4ed7911b291642 depends: - - python >=3.9 + - python >=3.10 - python-dateutil >=2.7.0 - - types-python-dateutil >=2.8.10 + - python-tzdata + - python license: Apache-2.0 - license_family: Apache + license_family: APACHE purls: - pkg:pypi/arrow?source=hash-mapping - size: 99951 - timestamp: 1733584345583 -- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - sha256: 93b14414b3b3ed91e286e1cbe4e7a60c4e1b1c730b0814d1e452a8ac4b9af593 - md5: 8f587de4bcf981e26228f268df374a9b + size: 113854 + timestamp: 1760831179410 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 + md5: 9673a61a297b00016442e022d689faa6 depends: - - python >=3.9 + - python >=3.10 constrains: - - astroid >=2,<4 + - astroid >=2,<5 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/asttokens?source=hash-mapping - size: 28206 - timestamp: 1733250564754 -- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - sha256: 3b7233041e462d9eeb93ea1dfe7b18aca9c358832517072054bb8761df0c324b - md5: d9d0f99095a9bb7e3641bca8c6ad2ac7 + size: 28797 + timestamp: 1763410017955 +- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + sha256: ea8486637cfb89dc26dc9559921640cd1d5fd37e5e02c33d85c94572139f2efe + md5: b85e84cb64c762569cc1a760c2327e0a depends: - - python >=3.9 + - python >=3.10 - typing_extensions >=4.0.0 - python license: MIT license_family: MIT purls: - pkg:pypi/async-lru?source=hash-mapping - size: 17335 - timestamp: 1742153708859 -- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - sha256: 99c53ffbcb5dc58084faf18587b215f9ac8ced36bbfb55fa807c00967e419019 - md5: a10d11958cadc13fdb43df75f8b1903f + size: 22949 + timestamp: 1773926359134 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab + md5: c6b0543676ecb1fb2d7643941fe375f2 depends: - - python >=3.9 + - python >=3.10 + - python license: MIT license_family: MIT purls: - pkg:pypi/attrs?source=compressed-mapping - size: 57181 - timestamp: 1741918625732 -- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - sha256: 1c656a35800b7f57f7371605bc6507c8d3ad60fbaaec65876fce7f73df1fc8ac - md5: 0a01c169f0ab0f91b26e77a3301fbfe4 - depends: - - python >=3.9 - - pytz >=2015.7 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/babel?source=compressed-mapping - size: 6938256 - timestamp: 1738490268466 + size: 64927 + timestamp: 1773935801332 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 md5: f1976ce927373500cc19d3c0b2c85177 @@ -2646,6 +2628,34 @@ packages: - pkg:pypi/babel?source=compressed-mapping size: 7684321 timestamp: 1772555330347 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda + sha256: e8c83696e6529ac1909a96690c58624bb376312fd0768409380cd9b05e248c9b + md5: 542da724e75cdeef19e29cca23935c25 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 238360 + timestamp: 1777848717715 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py313h18e8e13_0.conda + sha256: f92ccaef33713bb66d563e8e829dcdb643e1de8c0d29246a999943c68bb8eb6e + md5: cfe95f3eab50bc3dbd4c3a03f2674bd8 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 241034 + timestamp: 1777848719564 - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda noarch: generic sha256: de1755a35258eb1b59f2288559bbf0b76da60bd2fa6cd6f768ead442f85bd666 @@ -2656,19 +2666,88 @@ packages: purls: [] size: 7546 timestamp: 1777848733980 -- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - sha256: 4ce42860292a57867cfc81a5d261fb9886fc709a34eca52164cc8bbf6d03de9f - md5: 373374a3ed20141090504031dc7b693e +- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.4.0-py312h5f4ecc6_0.conda + sha256: 2e23f0834179520ce0d29f9157e42cd2aebc46320bffb7d31acb30493c7a392b + md5: d6e978e0a846f5a432eba7d51e1b36f2 depends: - - python >=3.9 - - soupsieve >=1.2 - - typing-extensions - license: MIT - license_family: MIT + - python + - __osx >=11.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=hash-mapping + size: 239098 + timestamp: 1777848754854 +- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.4.0-py313h116f8bd_0.conda + sha256: 553b05a3b42d16873248629c71b939b76a076d33aff91a5d2714583f1e5c7ed4 + md5: 1e37c8d0616c3964d3b7e47ecaa70294 + depends: + - python + - __osx >=11.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=hash-mapping + size: 241826 + timestamp: 1777848790321 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_0.conda + sha256: 7dbd64d3f06622ef8286be6dfceeb8e6008450fb4e6d9309fbb908b12f3937ff + md5: 95a833465ec45ac1e8f2ed1aaba8ec37 + depends: + - python + - __osx >=11.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 239305 + timestamp: 1777848727027 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py313h7208f8c_0.conda + sha256: 257b234caffc760e48c8d7e642d6d0d8bac4341ca19c7df098421358eff636a1 + md5: 84e922fe79295051f6925d7f8d71c98c + depends: + - python + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 242188 + timestamp: 1777848729476 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py312h06d0912_0.conda + sha256: 71caf40c0fdeb11fafaac639e6e6f9120112aa105a7a5e9dfb5b4b06db9ca97a + md5: 77d0a2bdd46dd8d502bb27eb80353fcd + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 237107 + timestamp: 1777848740547 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py313h2a31948_0.conda + sha256: b9c1465c03cb6ff79fb4f2feb21955b9b89783f868c900cad45dbcb8d81ab429 + md5: ef16917e1231df5c6f2ba245c8fe35e4 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/beautifulsoup4?source=compressed-mapping - size: 145482 - timestamp: 1738740460562 + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 240632 + timestamp: 1777848740331 - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 md5: 5267bef8efea4127aacd1f4e1f149b6e @@ -2682,64 +2761,64 @@ packages: - pkg:pypi/beautifulsoup4?source=hash-mapping size: 90399 timestamp: 1764520638652 -- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - sha256: a05971bb80cca50ce9977aad3f7fc053e54ea7d5321523efc7b9a6e12901d3cd - md5: f0b4c8e370446ef89797608d60a564b3 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + sha256: f8ff1f98423674278964a46c93a1766f9e91960d44efd91c6c3ed56a33813f46 + md5: 7c5ebdc286220e8021bf55e6384acd67 depends: - - python >=3.9 + - python >=3.10 - webencodings - python constrains: - - tinycss >=1.1.0,<1.5 + - tinycss2 >=1.1.0,<1.5 license: Apache-2.0 AND MIT purls: - pkg:pypi/bleach?source=hash-mapping - size: 141405 - timestamp: 1737382993425 -- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - sha256: 0aba699344275b3972bd751f9403316edea2ceb942db12f9f493b63c74774a46 - md5: a30e9406c873940383555af4c873220d + size: 142008 + timestamp: 1770719370680 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + sha256: 7c07a865e5e4cca233cc4e0eb3f0f5ff6c90776461687b4fb0b1764133e1fd61 + md5: f11a319b9700b203aa14c295858782b6 depends: - - bleach ==6.2.0 pyh29332c3_4 + - bleach ==6.3.0 pyhcf101f3_1 - tinycss2 license: Apache-2.0 AND MIT purls: [] - size: 4213 - timestamp: 1737382993425 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_2.conda - sha256: f2a59ccd20b4816dea9a2a5cb917eb69728271dbf1aeab4e1b7e609330a50b6f - md5: b0b867af6fc74b2a0aa206da29c0f3cf + size: 4409 + timestamp: 1770719370682 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda + sha256: 49df13a1bb5e388ca0e4e87022260f9501ed4192656d23dc9d9a1b4bf3787918 + md5: 64088dffd7413a2dd557ce837b4cbbdb depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 + - libgcc >=14 + - libstdcxx >=14 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 constrains: - - libbrotlicommon 1.1.0 hb9d3cd8_2 + - libbrotlicommon 1.2.0 hb03c661_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 349867 - timestamp: 1725267732089 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h46c70d0_2.conda - sha256: da92e5e904465fce33a7a55658b13caa5963cc463c430356373deeda8b2dbc46 - md5: f6bb3742e17a4af0dc3c8ca942683ef6 + size: 368300 + timestamp: 1764017300621 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + sha256: dadec2879492adede0a9af0191203f9b023f788c18efd45ecac676d424c458ae + md5: 6c4d3597cf43f3439a51b2b13e29a4ba depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.13.0rc1,<3.14.0a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 constrains: - - libbrotlicommon 1.1.0 hb9d3cd8_2 + - libbrotlicommon 1.2.0 hb03c661_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 350424 - timestamp: 1725267803672 + size: 367721 + timestamp: 1764017371123 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py314h3de4e8d_1.conda sha256: 3ad3500bff54a781c29f16ce1b288b36606e2189d0b0ef2f67036554f47f12b0 md5: 8910d2c46f7e7b519129f486e0fe927a @@ -2757,38 +2836,38 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 367376 timestamp: 1764017265553 -- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312h5861a67_2.conda - sha256: 265764ff4ad9e5cfefe7ea85c53d95157bf16ac2c0e5f190c528e4c9c0c1e2d0 - md5: b95025822e43128835826ec0cc45a551 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda + sha256: 8854a80360128157e8d05eb57c1c7e7c1cb10977e4c4557a77d29c859d1f104b + md5: 01fdbccc39e0a7698e9556e8036599b7 depends: - __osx >=10.13 - - libcxx >=17 + - libcxx >=19 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 constrains: - - libbrotlicommon 1.1.0 h00291cd_2 + - libbrotlicommon 1.2.0 h8616949_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 363178 - timestamp: 1725267893889 -- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py313h9ea2907_2.conda - sha256: a8ff547af4de5d2d6cb84543a73f924dbbd60029920dbadc27298ea0b48f28bc - md5: 38ab121f341a1d8613c3898f36efecab + size: 389534 + timestamp: 1764017976737 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py313h8d69aa9_1.conda + sha256: 3d328413ff65a12af493066d721d12f5ee82a0adf3565629ce4c797c4680162c + md5: 7c5e382b4d5161535f1dd258103fea51 depends: - __osx >=10.13 - - libcxx >=17 - - python >=3.13.0rc1,<3.14.0a0 + - libcxx >=19 + - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 constrains: - - libbrotlicommon 1.1.0 h00291cd_2 + - libbrotlicommon 1.2.0 h8616949_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 363156 - timestamp: 1725268004102 + size: 389859 + timestamp: 1764018040907 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py314h3262eb8_1.conda sha256: 2e34922abda4ac5726c547887161327b97c3bbd39f1204a5db162526b8b04300 md5: 389d75a294091e0d7fa5a6fc683c4d50 @@ -2805,40 +2884,40 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 390153 timestamp: 1764017784596 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hde4cb15_2.conda - sha256: 254b411fa78ccc226f42daf606772972466f93e9bc6895eabb4cfda22f5178af - md5: a83c2ef76ccb11bc2349f4f17696b15d +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda + sha256: 6178775a86579d5e8eec6a7ab316c24f1355f6c6ccbe84bb341f342f1eda2440 + md5: 311fcf3f6a8c4eb70f912798035edd35 depends: - __osx >=11.0 - - libcxx >=17 + - libcxx >=19 - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 constrains: - - libbrotlicommon 1.1.0 hd74edd7_2 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 339360 - timestamp: 1725268143995 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313h3579c5c_2.conda - sha256: b0a66572f44570ee7cc960e223ca8600d26bb20cfb76f16b95adf13ec4ee3362 - md5: f3bee63c7b5d041d841aff05785c28b7 + size: 359503 + timestamp: 1764018572368 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + sha256: 2e21dccccd68bedd483300f9ab87a425645f6776e6e578e10e0dd98c946e1be9 + md5: b03732afa9f4f54634d94eb920dfb308 depends: - __osx >=11.0 - - libcxx >=17 - - python >=3.13.0rc1,<3.14.0a0 - - python >=3.13.0rc1,<3.14.0a0 *_cp313 + - libcxx >=19 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 constrains: - - libbrotlicommon 1.1.0 hd74edd7_2 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 339067 - timestamp: 1725268603536 + size: 359568 + timestamp: 1764018359470 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py314h3daef5d_1.conda sha256: 5c2e471fd262fcc3c5a9d5ea4dae5917b885e0e9b02763dbd0f0d9635ed4cb99 md5: f9501812fe7c66b6548c7fcaa1c1f252 @@ -2856,40 +2935,40 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 359854 timestamp: 1764018178608 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_2.conda - sha256: f83baa6f6bcba7b73f6921d5c1aa95ffc5d8b246ade933ade79250de0a4c9c4c - md5: a99aec1ac46794a5fb1cd3cf5d2b6110 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda + sha256: 2bb6f384a51929ef2d5d6039fcf6c294874f20aaab2f63ca768cbe462ed4b379 + md5: e8e7a6346a9e50d19b4daf41f367366f depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - libbrotlicommon 1.1.0 h2466b09_2 + - libbrotlicommon 1.2.0 hfd05255_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 321874 - timestamp: 1725268491976 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313h5813708_2.conda - sha256: e89803147849d429f1ba3eec880b487c2cc4cac48a221079001a2ab1216f3709 - md5: c1a5d95bf18940d2b1d12f7bf2fb589b + size: 335482 + timestamp: 1764018063640 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + sha256: 3558006cd6e836de8dff53cbe5f0b9959f96ea6a6776b4e14f1c524916dd956c + md5: 916a39a0261621b8c33e9db2366dd427 depends: - - python >=3.13.0rc1,<3.14.0a0 + - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - libbrotlicommon 1.1.0 h2466b09_2 + - libbrotlicommon 1.2.0 hfd05255_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 322309 - timestamp: 1725268431915 + size: 335605 + timestamp: 1764018132514 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py314he701e3d_1.conda sha256: 6854ee7675135c57c73a04849c29cbebc2fb6a3a3bfee1f308e64bf23074719b md5: 1302b74b93c44791403cbeee6a0f62a3 @@ -2907,17 +2986,6 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 335782 timestamp: 1764018443683 -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d - md5: 62ee74e96c5ebb0af99386de58cf9553 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 252783 - timestamp: 1720974456583 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 md5: d2ffd7602c02f2b316fd921d39876885 @@ -2939,26 +3007,6 @@ packages: purls: [] size: 133427 timestamp: 1771350680709 -- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda - sha256: cad153608b81fb24fc8c509357daa9ae4e49dfc535b2cb49b91e23dbd68fc3c5 - md5: 7ed4301d437b59045be7e051a0308211 - depends: - - __osx >=10.13 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 134188 - timestamp: 1720974491916 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 - md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab - depends: - - __osx >=11.0 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 122909 - timestamp: 1720974522888 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df md5: 620b85a3f45526a8bc4d23fd78fc22f0 @@ -2981,25 +3029,6 @@ packages: purls: [] size: 56115 timestamp: 1771350256444 -- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - sha256: 35a5dad92e88fdd7fc405e864ec239486f4f31eec229e31686e61a140a8e573b - md5: 276e7ffe9ffe39688abc665ef0f45596 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 54927 - timestamp: 1720974860185 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - sha256: bf832198976d559ab44d6cdb315642655547e26d826e34da67cbee6624cda189 - md5: 19f3a56f68d2fd06c516076bff482c52 - license: ISC - purls: [] - size: 158144 - timestamp: 1738298224464 - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda sha256: 6f4ff81534c19e76acf52fcabf4a258088a932b8f1ac56e9a59e98f6051f8e46 md5: 56fb2c6c73efc627b40c77d14caecfba @@ -3018,27 +3047,6 @@ packages: purls: [] size: 131039 timestamp: 1776865545798 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2025.1.31-h8857fd0_0.conda - sha256: 42e911ee2d8808eacedbec46d99b03200a6138b8e8a120bd8acabe1cac41c63b - md5: 3418b6c8cac3e71c0bc089fc5ea53042 - license: ISC - purls: [] - size: 158408 - timestamp: 1738298385933 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda - sha256: 7e12816618173fe70f5c638b72adf4bfd4ddabf27794369bb17871c5bb75b9f9 - md5: 3569d6a9141adc64d2fe4797f3289e06 - license: ISC - purls: [] - size: 158425 - timestamp: 1738298167688 -- conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2025.1.31-h56e8100_0.conda - sha256: 1bedccdf25a3bd782d6b0e57ddd97cdcda5501716009f2de4479a779221df155 - md5: 5304a31607974dfc2110dfbb662ed092 - license: ISC - purls: [] - size: 158690 - timestamp: 1738298232550 - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 noarch: python sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 @@ -3061,16 +3069,6 @@ packages: - pkg:pypi/cached-property?source=hash-mapping size: 11065 timestamp: 1615209567874 -- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - sha256: 42a78446da06a2568cb13e69be3355169fbd0ea424b00fc80b7d840f5baaacf3 - md5: c207fa5ac7ea99b149344385a9c0880d - depends: - - python >=3.9 - license: ISC - purls: - - pkg:pypi/certifi?source=compressed-mapping - size: 162721 - timestamp: 1739515973129 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878 md5: 929471569c93acefb30282a22060dcd5 @@ -3081,13 +3079,13 @@ packages: - pkg:pypi/certifi?source=compressed-mapping size: 135656 timestamp: 1776866680878 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - sha256: cba6ea83c4b0b4f5b5dc59cb19830519b28f95d7ebef7c9c5cf1c14843621457 - md5: a861504bbea4161a9170b85d4d2be840 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py312h460c074_1.conda + sha256: 7dafe8173d5f94e46cf9cd597cc8ff476a8357fbbd4433a8b5697b2864845d9c + md5: 648ee28dcd4e07a1940a17da62eccd40 depends: - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 - pycparser - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 @@ -3095,30 +3093,30 @@ packages: license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 294403 - timestamp: 1725560714366 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda - sha256: 73cd6199b143a8a6cbf733ce124ed57defc1b9a7eab9b10fd437448caf8eaa45 - md5: ce6386a5892ef686d6d680c345c40ad1 + size: 295716 + timestamp: 1761202958833 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + sha256: 2162a91819945c826c6ef5efe379e88b1df0fe9a387eeba23ddcf7ebeacd5bd6 + md5: d0616e7935acab407d1543b28c446f6f depends: - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 - pycparser - - python >=3.13.0rc1,<3.14.0a0 + - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 295514 - timestamp: 1725560706794 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py312hf857d28_0.conda - sha256: 94fe49aed25d84997e2630d6e776a75ee2a85bd64f258702c57faa4fe2986902 - md5: 5bbc69b8194fedc2792e451026cac34f + size: 298357 + timestamp: 1761202966461 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py312he90777b_1.conda + sha256: e2888785e50ef99c63c29fb3cfbfb44cdd50b3bb7cd5f8225155e362c391936f + md5: cf70c8244e7ceda7e00b1881ad7697a9 depends: - __osx >=10.13 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - pycparser - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 @@ -3126,29 +3124,29 @@ packages: license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 282425 - timestamp: 1725560725144 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py313h49682b3_0.conda - sha256: 660c8f8488f78c500a1bb4a803c31403104b1ee2cabf1476a222a3b8abf5a4d7 - md5: 98afc301e6601a3480f9e0b9f8867ee0 + size: 288241 + timestamp: 1761203170357 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py313hf57695f_1.conda + sha256: 16c8c80bebe1c3d671382a64beaa16996e632f5b75963379e2b084eb6bc02053 + md5: b10f64f2e725afc9bf2d9b30eff6d0ea depends: - __osx >=10.13 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - pycparser - - python >=3.13.0rc1,<3.14.0a0 + - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 284540 - timestamp: 1725560667915 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda - sha256: 8d91a0d01358b5c3f20297c6c536c5d24ccd3e0c2ddd37f9d0593d0f0070226f - md5: 19a5456f72f505881ba493979777b24e + size: 290946 + timestamp: 1761203173891 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py312h1b4d9a2_1.conda + sha256: 597e986ac1a1bd1c9b29d6850e1cdea4a075ce8292af55568952ec670e7dd358 + md5: 503ac138ad3cfc09459738c0f5750705 depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - pycparser - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython @@ -3157,67 +3155,56 @@ packages: license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 281206 - timestamp: 1725560813378 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py313hc845a76_0.conda - sha256: 50650dfa70ccf12b9c4a117d7ef0b41895815bb7328d830d667a6ba3525b60e8 - md5: 6d24d5587a8615db33c961a4ca0a8034 + size: 288080 + timestamp: 1761203317419 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py314h44086f9_1.conda + sha256: 5b5ee5de01eb4e4fd2576add5ec9edfc654fbaf9293e7b7ad2f893a67780aa98 + md5: 10dd19e4c797b8f8bdb1ec1fbb6821d7 depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - pycparser - - python >=3.13.0rc1,<3.14.0a0 - - python >=3.13.0rc1,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 282115 - timestamp: 1725560759157 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py312h4389bb4_0.conda - sha256: ac007bf5fd56d13e16d95eea036433012f2e079dc015505c8a79efebbad1fcbc - md5: 08310c1a22ef957d537e547f8d484f92 + size: 292983 + timestamp: 1761203354051 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda + sha256: 3e3bdcb85a2e79fe47d9c8ce64903c76f663b39cb63b8e761f6f884e76127f82 + md5: 46f7dccfee37a52a97c0ed6f33fcf0a3 depends: - pycparser - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 288142 - timestamp: 1725560896359 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py313ha7868ed_0.conda - sha256: b19f581fe423858f1f477c52e10978be324c55ebf2e418308d30d013f4a476ff - md5: 519a29d7ac273f8c165efc0af099da42 + size: 291324 + timestamp: 1761203195397 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py314h5a2d7ad_1.conda + sha256: 924f2f01fa7a62401145ef35ab6fc95f323b7418b2644a87fea0ea68048880ed + md5: c360170be1c9183654a240aadbedad94 depends: - pycparser - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 291828 - timestamp: 1725561211547 -- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - sha256: 4e0ee91b97e5de3e74567bdacea27f0139709fceca4db8adffbe24deffccb09b - md5: e83a31202d1c0a000fce3e9cf3825875 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/charset-normalizer?source=hash-mapping - size: 47438 - timestamp: 1735929811779 + size: 294731 + timestamp: 1761203441365 - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 md5: a9167b9571f3baa9d448faa2139d1089 @@ -3229,17 +3216,6 @@ packages: - pkg:pypi/charset-normalizer?source=compressed-mapping size: 58872 timestamp: 1775127203018 -- conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.1-pyhd8ed1ab_0.conda - sha256: 21ecead7268241007bf65691610cd7314da68c1f88113092af690203b5780db5 - md5: 364ba6c9fb03886ac979b482f39ebb92 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/cloudpickle?source=hash-mapping - size: 25870 - timestamp: 1736947650712 - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 md5: 962b9857ee8e7018c22f2776ffa0b2d7 @@ -3251,18 +3227,48 @@ packages: - pkg:pypi/colorama?source=hash-mapping size: 27011 timestamp: 1733218222191 -- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - sha256: 7e87ef7c91574d9fac19faedaaee328a70f718c9b4ddadfdc0ba9ac021bd64af - md5: 74673132601ec2b7fc592755605f4c1b +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 depends: - python >=3.9 - - traitlets >=5.3 + - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/comm?source=hash-mapping - size: 12103 - timestamp: 1733503053903 + - pkg:pypi/comm?source=hash-mapping + size: 14690 + timestamp: 1753453984907 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py312h8a5da7c_0.conda + sha256: 9e88f91f85f0049686796fd25b20001bfbe9e4367714bb5d258849abcf54a705 + md5: c4d858e15305e70b255e756a4dc96e58 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 387585 + timestamp: 1773761191371 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py313h3dea7bd_0.conda + sha256: 4b38c6648d0ccd6dca1d1e0d826609aaf2fabfd662257c1fff00bdd0e69e02da + md5: acbda45380f5097ade59014704eb0ba0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 395334 + timestamp: 1773760969371 - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.5-py314h67df5f8_0.conda sha256: cf5f98a291c3a5489cb299bae38711d5dc21b88a00df981f3b1528781e18c909 md5: 78f547b78ace7541c4f54c4268ac9d2e @@ -3278,27 +3284,25 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 411308 timestamp: 1773761119353 -- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py312h178313f_0.conda - sha256: 5c502e6a72f46af9e6dd74e9d91449898c72ccb36dad46db7a09101042eef0c2 - md5: c9c5941aa3ad8c8324edf65128121395 +- conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py312heb39f77_0.conda + sha256: 29c0e4ad60442d4604bcea0b600f00d2f83392c1a6a0486fdf538dddb8d794d1 + md5: dc7d28998f3551b92ad6222712dd9760 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - __osx >=11.0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - tomli license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/coverage?source=compressed-mapping - size: 372061 - timestamp: 1742591755385 -- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.7.1-py313h8060acc_0.conda - sha256: 0b94ba88404ff65eb95f881c09a3e214b28c91a93af0e3c5c2cc30eba5a6dfb0 - md5: 2c6a4bb9f97e785db78f9562cdf8b3af + - pkg:pypi/coverage?source=hash-mapping + size: 387495 + timestamp: 1773761104781 +- conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py313h035b7d0_0.conda + sha256: e5c7ba0e9fdc80c64975d47da23b4bec2aeade29e1f3b734fe2cf547535c99c2 + md5: 253be7e7dddee10871606824cbd7208f depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - __osx >=11.0 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - tomli @@ -3306,8 +3310,8 @@ packages: license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 378570 - timestamp: 1742591809856 + size: 394683 + timestamp: 1773761109302 - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.13.5-py314h77fa6c7_0.conda sha256: fc472d8fcc0e831faf1af1ed78f4db9bd62f311187f05d1c140626a9317b8a4e md5: c5d3ea7d5f490c69a6af7c056624fca4 @@ -3322,34 +3326,36 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 412623 timestamp: 1773761406443 -- conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py312h3520af0_0.conda - sha256: 0153ea1d55d1c4a88a9a7e6ba24332cdac3379c2e62874d7af91e8586606ede0 - md5: 3aac48d735f438a582078c623c1f6a70 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py312h04c11ed_0.conda + sha256: dc131457c66f0aada1cbc8fb1ed836944f6643f16c1c99769527d9ebc665cf81 + md5: 8d13c0860b184f2bdaa261173167fb35 depends: - - __osx >=10.13 + - __osx >=11.0 - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - tomli license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 369837 - timestamp: 1742591983990 -- conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.7.1-py313h717bdf5_0.conda - sha256: 50c3d5b2bd9c42ae88549be25ee0584050116f61c7c1eab136fe340a9163b2d6 - md5: 2db779f3f09f1091b9a6d3007634ec08 + size: 387595 + timestamp: 1773761387421 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py313h65a2061_0.conda + sha256: 81b811c845e5a585812af256da9c2488d65f520bb74e4bb766d031fda15b8957 + md5: 0dbaee2d94c17eb38562c9b74d9b31ec depends: - - __osx >=10.13 + - __osx >=11.0 - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - tomli license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 377981 - timestamp: 1742591939877 + size: 394533 + timestamp: 1773761662394 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.13.5-py314h6e9b3f0_0.conda sha256: 808ebcb57027251f379f84e53a3755d2851918f78bdd512d131afe40ca64a041 md5: cdbafe4a3e605024e7372c9580f9d734 @@ -3365,36 +3371,38 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 412458 timestamp: 1773761280047 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py312h998013c_0.conda - sha256: 9da0faf9092eea8ae195b0316c039a1e59a9e10c43d4a16660b70341515b15bb - md5: 07426bb994e08ea760003047e7e6f68f +- conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py312h05f76fc_0.conda + sha256: 1a232970b9fa840efd3d5fb55760c1afc18335feb20b8da8c8e16d0418bd6cf0 + md5: 24b75aab5a8c2df25695ebee2b5ffa49 depends: - - __osx >=11.0 - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 - tomli + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 370369 - timestamp: 1742591989921 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.7.1-py313ha9b7d5b_0.conda - sha256: 11e43afb5d0684db36b5c9eec2667355240e468c668cf90b0be54be8c2fda0ce - md5: 7b4f5e8345f3f28d3058757452b7975e + size: 414438 + timestamp: 1773760980441 +- conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py313hd650c13_0.conda + sha256: a96787dec7bebe3acd7723fbcc061364672abec5d78e279005b467bd1c93053c + md5: 94e2634e6ba6eb34dd0917d47b05ba0a depends: - - __osx >=11.0 - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 - tomli + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/coverage?source=hash-mapping - size: 378772 - timestamp: 1742591852148 + size: 420154 + timestamp: 1773761008665 - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.13.5-py314h2359020_0.conda sha256: 80a6a7be7eef784b8314a4cb563563c654e2180a0b2b31b232f79b2e7334aaf2 md5: 849f0bd5b83d4fd59b41202b21bb3ca2 @@ -3411,60 +3419,28 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 438927 timestamp: 1773760993379 -- conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py312h31fea79_0.conda - sha256: 135bd1283053bb0f83f5166d17a13cc9c73c6d8af2d3e09f57d360a81413f0af - md5: b2d00351ff17283c886c65f1121c21a4 - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - tomli - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/coverage?source=hash-mapping - size: 397481 - timestamp: 1742592161824 -- conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.7.1-py313hb4c8b1a_0.conda - sha256: 4e9be2a1e71786c27fe52926fa15d3b98124df15e84444bcd73a7bd2de405d13 - md5: 4df539b2dafaf01ffb8c222b87867d24 - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - tomli - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/coverage?source=hash-mapping - size: 403906 - timestamp: 1742592209260 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.9-py312hd8ed1ab_1.conda +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.13-py312hd8ed1ab_0.conda noarch: generic - sha256: 58a637bc8328b115c9619de3fcd664ec26662083319e3c106917a1b3ee4d7594 - md5: f0f8087079679f3ae375fca13327b17f + sha256: d3e9bbd7340199527f28bbacf947702368f31de60c433a16446767d3c6aaf6fe + md5: f54c1ffb8ecedb85a8b7fcde3a187212 depends: - - python 3.12.9.* + - python >=3.12,<3.13.0a0 - python_abi * *_cp312 license: Python-2.0 purls: [] - size: 45728 - timestamp: 1741128060593 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.2-py313hd8ed1ab_101.conda + size: 46463 + timestamp: 1772728929620 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.13-py313hd8ed1ab_100.conda noarch: generic - sha256: 29bfebfbd410db5e90fa489b239a3a7473bc1ec776bdca24e8c26c68c5654a8c - md5: d6be72c63da6e99ac2a1b87b120d135a + sha256: 836b92c209d4b6b9fb28bd51bd788b22a0c5492ae95eec2724e65a15ed4ab2e1 + md5: 3a8a8b87e72f95b54689fb588e154ec9 depends: - - python 3.13.2.* + - python >=3.13,<3.14.0a0 - python_abi * *_cp313 license: Python-2.0 purls: [] - size: 47792 - timestamp: 1739800762370 + size: 48530 + timestamp: 1775613723457 - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda noarch: generic sha256: 40dc224f2b718e5f034efd2332bc315a719063235f63673468d26a24770094ee @@ -3476,95 +3452,124 @@ packages: purls: [] size: 49809 timestamp: 1775614256655 -- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda - sha256: 8f0b338687f79ea87324f067bedddd2168f07b8eec234f0fe63b522344c6a919 - md5: 089cf3a3becf0e2f403feaf16e921678 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py312h8285ef7_0.conda + sha256: f20121b67149ff80bf951ccae7442756586d8789204cd08ade59397b22bfd098 + md5: ee1b48795ceb07311dd3e665dd4f5f33 depends: + - python + - libgcc >=14 + - libstdcxx >=14 - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/debugpy?source=hash-mapping - size: 2630748 - timestamp: 1744321406939 -- conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.14-py312haafddd8_0.conda - sha256: b1c9f30148045219844f947fe43d4ee19c4cc6ee83e7518b2e83db780d3e97e6 - md5: a3831727ed5b148d096afb80a6009cab + size: 2858582 + timestamp: 1769744978783 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda + sha256: 8d76d4eeb5a8e3c5666880b465593fdf4a44f47fbbe89ff5b8f9abbe43026326 + md5: e94dbbec2589f3b1d821f43a4bf2ab45 + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2872698 + timestamp: 1769744980407 +- conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.20-py312h29de90a_0.conda + sha256: 310f737be38bd4a53b2c13c6387b880031fc0995a13194511cc08a48d3160462 + md5: 4e508cd9d5d630c7db0bdebb24a3be90 depends: + - python + - libcxx >=19 - __osx >=10.13 - - libcxx >=18 - - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/debugpy?source=hash-mapping - size: 2557869 - timestamp: 1744321625095 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py312hd8f9ff3_0.conda - sha256: c833d92953a4c747f2606cefaebdbeaeec7c8d374bb7652dd0cc241cb120fdbc - md5: f1be818f2cee62e6edc12d5aaae13f57 + size: 2764546 + timestamp: 1769744989784 +- conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.20-py313h8b5a893_0.conda + sha256: 50e6280b8fc3eca1dad3a03deb7bb861c34c61e85331f3ff37f1faed833e968a + md5: d97267b6016ad4bfb48874defeab29ea + depends: + - python + - __osx >=10.13 + - libcxx >=19 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2771547 + timestamp: 1769745020308 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py312h6510ced_0.conda + sha256: f0ca130b5ffd6949673d3c61d7b8562ab76ad8debafb83f8b3443d30c172f5eb + md5: da3b5efcb0caabcede61a6ce4e0a7669 depends: + - python - __osx >=11.0 - - libcxx >=18 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython + - python 3.12.* *_cpython + - libcxx >=19 - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/debugpy?source=hash-mapping - size: 2581221 - timestamp: 1744321582400 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py313h928ef07_0.conda - sha256: e1fef24f7d220dd77522f06598d2c8c5b6ca68123f06515436c57a8777871481 - md5: 6521542d1c40d124657586810f220571 + size: 2752978 + timestamp: 1769744996462 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.20-py314he609de1_0.conda + sha256: 7736a82ebe75c0f3ea6991298363d1f2edb34291f8616c1d3719862881c3a167 + md5: 407c74dc27356ba6bf3a0191070e3ac0 depends: + - python + - python 3.14.* *_cp314 - __osx >=11.0 - - libcxx >=18 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - libcxx >=19 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT purls: - pkg:pypi/debugpy?source=hash-mapping - size: 2534826 - timestamp: 1744321649930 -- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py312h275cf98_0.conda - sha256: 02ceea9c12eaaf29c7c40142e4789b77c5c98aa477bdfca1db3ae97440b9e2fe - md5: 331737db69ae5431acb6ef3e198ec623 + size: 2778080 + timestamp: 1769745040206 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py312ha1a9051_0.conda + sha256: 5a886b1af3c66bf58213c7f3d802ea60fe8218313d9072bc1c9e8f7840548ba0 + md5: 032746a0b0663920f0afb18cec61062b depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/debugpy?source=hash-mapping - size: 3561750 - timestamp: 1744321803729 -- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py313h5813708_0.conda - sha256: dafd02b080118f11c7aea830d8e1c263134b90cf7e5518440fab46992130c100 - md5: d5d1eaa5f605092cc407ed0bfb5e16bf + size: 3996113 + timestamp: 1769745013982 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.20-py314hb98de8c_0.conda + sha256: ece1d8299ad081edaf1e5279f2a900bdedddb2c795ac029a06401543cd7610ad + md5: 48ae8370a4562f7049d587d017792a3a depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT purls: - pkg:pypi/debugpy?source=hash-mapping - size: 3589078 - timestamp: 1744321801176 + size: 4026404 + timestamp: 1769745008861 - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 md5: 9ce473d1d1be1cc3810856a48b3fab32 @@ -3573,7 +3578,7 @@ packages: license: BSD-2-Clause license_family: BSD purls: - - pkg:pypi/decorator?source=compressed-mapping + - pkg:pypi/decorator?source=hash-mapping size: 14129 timestamp: 1740385067843 - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 @@ -3587,16 +3592,6 @@ packages: - pkg:pypi/defusedxml?source=hash-mapping size: 24062 timestamp: 1615232388757 -- conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - sha256: fa5966bb1718bbf6967a85075e30e4547901410cc7cb7b16daf68942e9a94823 - md5: 24c1ca34138ee57de72a943237cde4cc - depends: - - python >=3.9 - license: CC-PDDC AND BSD-3-Clause AND BSD-2-Clause AND ZPL-2.1 - purls: - - pkg:pypi/docutils?source=hash-mapping - size: 402700 - timestamp: 1733217860944 - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda sha256: 0d605569a77350fb681f9ed8d357cc71649b59a304099dc9d09fbeec5e84a65e md5: d6bd3cd217e62bbd7efe67ff224cd667 @@ -3607,29 +3602,18 @@ packages: - pkg:pypi/docutils?source=hash-mapping size: 438002 timestamp: 1766092633160 -- conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.36.0-pyhd8ed1ab_1.conda - sha256: 8925dc378a2d5533905b478c69fd7ea7c72c664aa4b37075a2711079bc9222e3 - md5: 18d4243b3d30352f9dea8e522f6ff4d1 +- conda: https://conda.anaconda.org/conda-forge/noarch/doit-0.37.0-pyhcf101f3_0.conda + sha256: ed23dc270abd9c51b83af377d3dc09e4a82fc85bb118b6fdaa88b5bc350854a9 + md5: 37b3d4c558f2bb2b5378c43f4d6f1fb5 depends: - - cloudpickle - - importlib-metadata >=4.4 - - python >=3.9 + - python >=3.10 + - python license: MIT license_family: MIT purls: - pkg:pypi/doit?source=hash-mapping - size: 73790 - timestamp: 1734618648157 -- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - sha256: cbde2c64ec317118fc06b223c5fd87c8a680255e7348dd60e7b292d2e103e701 - md5: a16662747cdeb9abbac74d0057cc976e - depends: - - python >=3.9 - license: MIT and PSF-2.0 - purls: - - pkg:pypi/exceptiongroup?source=hash-mapping - size: 20486 - timestamp: 1733208916977 + size: 78854 + timestamp: 1770674540299 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 md5: 8e662bd460bda79b1ea39194e3c4c9ab @@ -3641,17 +3625,6 @@ packages: - pkg:pypi/exceptiongroup?source=hash-mapping size: 21333 timestamp: 1763918099466 -- conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - sha256: 9abc6c128cd40733e9b24284d0462e084d4aff6afe614f0754aa8533ebe505e4 - md5: a71efeae2c160f6789900ba2631a2c90 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/execnet?source=hash-mapping - size: 38835 - timestamp: 1733231086305 - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda sha256: 1acc6a420efc5b64c384c1f35f49129966f8a12c93b4bb2bdc30079e5dc9d8a8 md5: a57b4be42619213a94f31d2c69c5dda7 @@ -3663,17 +3636,17 @@ packages: - pkg:pypi/execnet?source=hash-mapping size: 39499 timestamp: 1762974150770 -- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda - sha256: 7510dd93b9848c6257c43fdf9ad22adf62e7aa6da5f12a6a757aed83bcfedf05 - md5: 81d30c08f9a3e556e8ca9e124b044d14 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c depends: - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/executing?source=hash-mapping - size: 29652 - timestamp: 1745502200340 + size: 30753 + timestamp: 1756729456476 - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 md5: d3549fd50d450b6d9e7dddff25dd2110 @@ -3686,31 +3659,19 @@ packages: - pkg:pypi/fqdn?source=hash-mapping size: 16705 timestamp: 1733327494780 -- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - sha256: f64b68148c478c3bfc8f8d519541de7d2616bf59d44485a5271041d40c061887 - md5: 4b69232755285701bc86a5afe4d9933a +- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb + md5: b8993c19b0c32a2f7b66cbb58ca27069 depends: - - python >=3.9 + - python >=3.10 - typing_extensions + - python license: MIT license_family: MIT purls: - - pkg:pypi/h11?source=hash-mapping - size: 37697 - timestamp: 1745526482242 -- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - sha256: 0aa1cdc67a9fe75ea95b5644b734a756200d6ec9d0dff66530aec3d1c1e9df75 - md5: b4754fb1bdcb70c8fd54f918301582c6 - depends: - - hpack >=4.1,<5 - - hyperframe >=6.1,<7 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/h2?source=hash-mapping - size: 53888 - timestamp: 1738578623567 + - pkg:pypi/h11?source=compressed-mapping + size: 39069 + timestamp: 1767729720872 - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 @@ -3791,16 +3752,6 @@ packages: purls: [] size: 12723451 timestamp: 1773822285671 -- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - sha256: 2e64307532f482a0929412976c8450c719d558ba20c0962832132fd0d07ba7a7 - md5: d68d48a3060eb5abdc1cdc8e2a3a5966 - depends: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: [] - size: 11761697 - timestamp: 1720853679409 - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda sha256: 1294117122d55246bb83ad5b589e2a031aacdf2d0b1f99fd338aa4394f881735 md5: 627eca44e62e2b665eeec57a984a7f00 @@ -3811,27 +3762,6 @@ packages: purls: [] size: 12273764 timestamp: 1773822733780 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 - md5: 5eb22c1d7b3fc4abb50d92d621583137 - depends: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: [] - size: 11857802 - timestamp: 1720853997952 -- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 - md5: 39a4f67be3286c86d696df570b1201b7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/idna?source=hash-mapping - size: 49765 - timestamp: 1733211921194 - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48 md5: fb7130c190f9b4ec91219840a05ba3ac @@ -3844,17 +3774,6 @@ packages: - pkg:pypi/idna?source=compressed-mapping size: 59038 timestamp: 1776947141407 -- conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - sha256: c2bfd7043e0c4c12d8b5593de666c1e81d67b83c474a0a79282cc5c4ef845460 - md5: 7de5386c8fea29e76b303f37dde4c352 - depends: - - python >=3.4 - license: MIT - license_family: MIT - purls: - - pkg:pypi/imagesize?source=hash-mapping - size: 10164 - timestamp: 1656939625410 - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda sha256: 5a047f9eac290e679b4e6f6f4cbfcc5acdfbf031a4f06824d4ddb590cdbb850b md5: 92617c2ba2847cca7a6ed813b6f4ab79 @@ -3866,44 +3785,19 @@ packages: - pkg:pypi/imagesize?source=hash-mapping size: 15729 timestamp: 1773752188889 -- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745 - md5: 63ccfdc3a3ce25b027b8767eb722fca8 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16 + md5: 080594bf4493e6bae2607e65390c520a depends: - - python >=3.9 + - python >=3.10 - zipp >=3.20 - python license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/importlib-metadata?source=compressed-mapping - size: 34641 - timestamp: 1747934053147 -- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80 - md5: c85c76dc67d75619a92f51dfbce06992 - depends: - - python >=3.9 - - zipp >=3.1.0 - constrains: - - importlib-resources >=6.5.2,<6.5.3.0a0 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/importlib-resources?source=hash-mapping - size: 33781 - timestamp: 1736252433366 -- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - sha256: 0ec8f4d02053cd03b0f3e63168316530949484f80e16f5e2fb199a1d117a89ca - md5: 6837f3eff7dcea42ecd714ce1ac2b108 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/iniconfig?source=hash-mapping - size: 11474 - timestamp: 1733223232820 + size: 34387 + timestamp: 1773931568510 - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 md5: 9614359868482abba1bd15ce465e3c42 @@ -3915,129 +3809,136 @@ packages: - pkg:pypi/iniconfig?source=hash-mapping size: 13387 timestamp: 1760831448842 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - sha256: 33cfd339bb4efac56edf93474b37ddc049e08b1b4930cf036c893cc1f5a1f32a - md5: b40131ab6a36ac2c09b7c57d4d3fbf99 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda + sha256: 5c1f3e874adaf603449f2b135d48f168c5d510088c78c229bda0431268b43b27 + md5: 4b53d436f3fbc02ce3eeaf8ae9bebe01 depends: - - __linux + - appnope + - __osx - comm >=0.1.1 - debugpy >=1.6.5 - ipython >=7.23.1 - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* - matplotlib-inline >=0.1 - - nest-asyncio - - packaging - - psutil - - python >=3.8 - - pyzmq >=24 - - tornado >=6.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/ipykernel?source=hash-mapping - size: 119084 - timestamp: 1719845605084 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda - sha256: dc569094125127c0078aa536f78733f383dd7e09507277ef8bcd1789786e7086 - md5: 18df5fc4944a679e085e0e8f31775fc8 + size: 132260 + timestamp: 1770566135697 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh6dadd2b_1.conda + sha256: 9cdadaeef5abadca4113f92f5589db19f8b7df5e1b81cb0225f7024a3aedefa3 + md5: b3a7d5842f857414d9ae831a799444dd depends: - __win - comm >=0.1.1 - debugpy >=1.6.5 - ipython >=7.23.1 - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* - matplotlib-inline >=0.1 - - nest-asyncio - - packaging - - psutil - - python >=3.8 - - pyzmq >=24 - - tornado >=6.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/ipykernel?source=hash-mapping - size: 119853 - timestamp: 1719845858082 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda - sha256: 072534d4d379225b2c3a4e38bc7730b65ae171ac7f0c2d401141043336e97980 - md5: 9eb15d654daa0ef5a98802f586bb4ffc + size: 132382 + timestamp: 1770566174387 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + sha256: b77ed58eb235e5ad80e742b03caeed4bbc2a2ef064cb9a2deee3b75dfae91b2a + md5: 8b267f517b81c13594ed68d646fd5dcb depends: - - __osx - - appnope + - __linux - comm >=0.1.1 - debugpy >=1.6.5 - ipython >=7.23.1 - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* - matplotlib-inline >=0.1 - - nest-asyncio - - packaging - - psutil - - python >=3.8 - - pyzmq >=24 - - tornado >=6.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/ipykernel?source=hash-mapping - size: 119568 - timestamp: 1719845667420 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyh6be1c34_0.conda - sha256: b6189de4e9f3d007a11e6e1df023c2bb73cf1864f63ca154c5ff8f0cdf601a50 - md5: 73e4ba4c8247f744be670f4da4f132e2 + size: 133644 + timestamp: 1770566133040 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyh53cf698_0.conda + sha256: a0af49948a1842dfd15a0b0b2fd56c94ddbd07e07a6c8b4bc70d43015eafaff0 + md5: 73e9657cd19605740d21efb14d8d0cb9 depends: - - __win - - colorama - - decorator - - exceptiongroup - - ipython_pygments_lexers - - jedi >=0.16 - - matplotlib-inline - - pickleshare + - __unix + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 - prompt-toolkit >=3.0.41,<3.1.0 - - pygments >=2.4.0 + - psutil >=7 + - pygments >=2.14.0 - python >=3.11 - - stack_data + - stack_data >=0.6.0 - traitlets >=5.13.0 - typing_extensions >=4.6 + - pexpect >4.6 - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/ipython?source=hash-mapping - size: 621095 - timestamp: 1748711232331 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.3.0-pyhfa0c392_0.conda - sha256: ee5d526cba0c0a5981cbcbcadc37a76d257627a904ed2cd2db45821735c93ebd - md5: 270dbfb30fe759b39ce0c9fdbcd7be10 + - pkg:pypi/ipython?source=compressed-mapping + size: 651632 + timestamp: 1777038396606 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.13.0-pyhe2676ad_0.conda + sha256: f252ec33597115ff21cbb31051f6f9be34ca36cbbbf3d266b597660d8d8edde9 + md5: 5631ab99e902463d9dd4221e5b4eab6d depends: - - __unix - - pexpect >4.3 - - decorator - - exceptiongroup - - ipython_pygments_lexers - - jedi >=0.16 - - matplotlib-inline - - pickleshare + - __win + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 - prompt-toolkit >=3.0.41,<3.1.0 - - pygments >=2.4.0 + - psutil >=7 + - pygments >=2.14.0 - python >=3.11 - - stack_data + - stack_data >=0.6.0 - traitlets >=5.13.0 - typing_extensions >=4.6 + - colorama >=0.4.4 - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/ipython?source=compressed-mapping - size: 621859 - timestamp: 1748713870748 + size: 650593 + timestamp: 1777038425499 - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 md5: bd80ba060603cc228d9d81c257093119 @@ -4085,220 +3986,152 @@ packages: purls: - pkg:pypi/jinja2?source=hash-mapping size: 120685 - timestamp: 1764517220861 -- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af - md5: 446bd6c8cb26050d528881df495ce646 - depends: - - markupsafe >=2.0 - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jinja2?source=compressed-mapping - size: 112714 - timestamp: 1741263433881 -- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.0-pyhd8ed1ab_0.conda - sha256: 889e2a49de796475b5a4bc57d0ba7f4606b368ee2098e353a6d9a14b0e2c6393 - md5: 56275442557b3b45752c10980abfe2db - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/json5?source=hash-mapping - size: 34114 - timestamp: 1743722170015 -- conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py312h7900ff3_1.conda - sha256: 76ccb7bffc7761d1d3133ffbe1f7f1710a0f0d9aaa9f7ea522652e799f3601f4 - md5: 6b51f7459ea4073eeb5057207e2e1e3d - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 17277 - timestamp: 1725303032027 -- conda: https://conda.anaconda.org/conda-forge/osx-64/jsonpointer-3.0.0-py312hb401068_1.conda - sha256: 52fcb1db44a935bba26988cc17247a0f71a8ad2fbc2b717274a8c8940856ee0d - md5: 5dcf96bca4649d496d818a0f5cfb962e - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 17560 - timestamp: 1725303027769 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py312h81bd7bf_1.conda - sha256: f6fb3734e967d1cd0cde32844ee952809f6c0a49895da7ec1c8cfdf97739b947 - md5: 80f403c03290e1662be03e026fb5f8ab - depends: - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 17865 - timestamp: 1725303130815 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_1.conda - sha256: cc2f68ceb34bca53b7b9a3eb3806cc893ef8713a5a6df7edf7ff989f559ef81d - md5: f2757998237755a74a12680a4e6a6bd6 - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python >=3.13.0rc1,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 18232 - timestamp: 1725303194596 -- conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py312h2e8e312_1.conda - sha256: 6865b97780e795337f65592582aee6f25e5b96214c64ffd3f8cdf580fd64ba22 - md5: e3ceda014d8461a11ca8552830a978f9 - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.14.0-pyhd8ed1ab_0.conda + sha256: 9daa95bd164c8fa23b3ab196e906ef806141d749eddce2a08baa064f722d25fa + md5: 1269891272187518a0a75c286f7d0bbf + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 42235 - timestamp: 1725303419414 -- conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_1.conda - sha256: a0625cb0e86775b8996b4ee7117f1912b2fa3d76be8d10bf1d7b39578f5d99f7 - md5: 001efbf150f0ca5fd0a0c5e6e713c1d1 + - pkg:pypi/json5?source=compressed-mapping + size: 34731 + timestamp: 1774655440045 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + sha256: a3d10301b6ff399ba1f3d39e443664804a3d28315a4fb81e745b6817845f70ae + md5: 89bf346df77603055d3c8fe5811691e6 depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/jsonpointer?source=hash-mapping - size: 42805 - timestamp: 1725303293802 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda - sha256: 812134fabb49493a50f7f443dc0ffafd0f63766f403a0bd8e71119763e57456a - md5: 59220749abcd119d645e6879983497a1 + size: 14190 + timestamp: 1774311356147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc depends: - attrs >=22.2.0 - - importlib_resources >=1.4.0 - - jsonschema-specifications >=2023.03.6 - - pkgutil-resolve-name >=1.3.10 - - python >=3.9 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 - referencing >=0.28.4 - - rpds-py >=0.7.1 + - rpds-py >=0.25.0 + - python license: MIT license_family: MIT purls: - pkg:pypi/jsonschema?source=hash-mapping - size: 75124 - timestamp: 1748294389597 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda - sha256: 66fbad7480f163509deec8bd028cd3ea68e58022982c838683586829f63f3efa - md5: 41ff526b1083fde51fbdc93f29282e0e + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a depends: - - python >=3.9 + - python >=3.10 - referencing >=0.31.0 - python license: MIT license_family: MIT purls: - pkg:pypi/jsonschema-specifications?source=hash-mapping - size: 19168 - timestamp: 1745424244298 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.24.0-hd8ed1ab_0.conda - sha256: 970a1efffe29474d6bb3e4d63bc04105c5611d1c7e2cd7e2d43d1ba468f33c20 - md5: b4eaebf6fac318db166238796d2a9702 + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 + md5: 8368d58342d0825f0843dc6acdd0c483 depends: + - jsonschema >=4.26.0,<4.26.1.0a0 - fqdn - idna - isoduration - jsonpointer >1.13 - - jsonschema >=4.24.0,<4.24.1.0a0 - rfc3339-validator - rfc3986-validator >0.1.0 + - rfc3987-syntax >=1.1.0 - uri-template - webcolors >=24.6.0 license: MIT license_family: MIT purls: [] - size: 7717 - timestamp: 1748294391013 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhe01879c_2.conda - sha256: f2ca86b121bcfeaf0241a927824459ba8712e64806b98dd262eb2b1a7c4e82a6 - md5: 7ed6505c703f3c4e1a58864bf84505e2 + size: 4740 + timestamp: 1767839954258 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.1-pyhcf101f3_0.conda + sha256: 3766e2ae59641c172cec8a821528bfa6bf9543ffaaeb8b358bfd5259dcf18e4e + md5: 0c3b465ceee138b9c39279cc02e5c4a0 depends: - importlib-metadata >=4.8.3 - jupyter_server >=1.1.2 - - python >=3.9 + - python >=3.10 - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-lsp?source=compressed-mapping - size: 57659 - timestamp: 1748550130303 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - sha256: 19d8bd5bb2fde910ec59e081eeb59529491995ce0d653a5209366611023a0b3a - md5: 4ebae00eae9705b0c3d6d1018a81d047 + - pkg:pypi/jupyter-lsp?source=hash-mapping + size: 61633 + timestamp: 1775136333147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + sha256: e402bd119720862a33229624ec23645916a7d47f30e1711a4af9e005162b84f3 + md5: 8a3d6d0523f66cf004e563a50d9392b3 depends: - - importlib-metadata >=4.8.3 - - jupyter_core >=4.12,!=5.0.* - - python >=3.9 + - jupyter_core >=5.1 + - python >=3.10 - python-dateutil >=2.8.2 - - pyzmq >=23.0 - - tornado >=6.2 + - pyzmq >=25.0 + - tornado >=6.4.1 - traitlets >=5.3 + - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-client?source=hash-mapping - size: 106342 - timestamp: 1733441040958 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - sha256: 56a7a7e907f15cca8c4f9b0c99488276d4cb10821d2d15df9245662184872e81 - md5: b7d89d860ebcda28a5303526cdee68ab + - pkg:pypi/jupyter-client?source=compressed-mapping + size: 112785 + timestamp: 1767954655912 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc + md5: a8db462b01221e9f5135be466faeb3e0 depends: - - __unix + - __win + - pywin32 - platformdirs >=2.5 - - python >=3.8 + - python >=3.10 - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-core?source=compressed-mapping - size: 59562 - timestamp: 1748333186063 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda - sha256: 928c2514c2974fda78447903217f01ca89a77eefedd46bf6a2fe97072df57e8d - md5: 324e60a0d3f39f268e899709575ea3cd + - pkg:pypi/jupyter-core?source=hash-mapping + size: 64679 + timestamp: 1760643889625 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 depends: - - __win - - cpython + - __unix + - python - platformdirs >=2.5 - - python >=3.8 - - pywin32 >=300 + - python >=3.10 - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-core?source=compressed-mapping - size: 59972 - timestamp: 1748333368923 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - sha256: 37e6ac3ccf7afcc730c3b93cb91a13b9ae827fd306f35dd28f958a74a14878b5 - md5: f56000b36f09ab7533877e695e4e8cb0 + - pkg:pypi/jupyter-core?source=hash-mapping + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + sha256: c7edb5682c6316a95ad781dccb1b6589cd2ec0bf94f23c21152974eb0363b5d7 + md5: bf42ee94c750c0b2e7e998b79ac299ea depends: - jsonschema-with-format-nongpl >=4.18.0 - packaging - - python >=3.9 + - python >=3.10 - python-json-logger >=2.0.4 - pyyaml >=5.3 - referencing @@ -4309,12 +4142,12 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-events?source=compressed-mapping - size: 23647 - timestamp: 1738765986736 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.16.0-pyhe01879c_0.conda - sha256: 0082fb6f0afaf872affee4cde3b210f7f7497a5fb47f2944ab638fef0f0e2e77 - md5: f062e04d7cd585c937acbf194dceec36 + - pkg:pypi/jupyter-events?source=hash-mapping + size: 24002 + timestamp: 1776861872237 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.18.0-pyhcf101f3_0.conda + sha256: 953b8528e088ad3b38764c043d8c168d28583593a6a8dd02a1e8c1e4c860d378 + md5: 148450224bdca4f51bf4fe66c6e57cd7 depends: - anyio >=3.1.0 - argon2-cffi >=21.1 @@ -4328,7 +4161,7 @@ packages: - overrides >=5.0 - packaging >=22.0 - prometheus_client >=0.9 - - python >=3.9 + - python >=3.10 - pyzmq >=24 - send2trash >=1.8.2 - terminado >=0.8.3 @@ -4337,31 +4170,31 @@ packages: - websocket-client >=1.7 - python license: BSD-3-Clause - license_family: BSD purls: - - pkg:pypi/jupyter-server?source=hash-mapping - size: 344376 - timestamp: 1747083217715 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - sha256: 0890fc79422191bc29edf17d7b42cff44ba254aa225d31eb30819f8772b775b8 - md5: 2d983ff1b82a1ccb6f2e9d8784bdd6bd + - pkg:pypi/jupyter-server?source=compressed-mapping + size: 359130 + timestamp: 1777905221568 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 + md5: 7b8bace4943e0dc345fc45938826f2b8 depends: - - python >=3.9 + - python >=3.10 - terminado >=0.8.3 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/jupyter-server-terminals?source=hash-mapping - size: 19711 - timestamp: 1733428049134 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.3-pyhd8ed1ab_0.conda - sha256: fc0235a71d852734fe92183a78cb91827367573450eba82465ae522c64230736 - md5: 4861a0c2a5a5d0481a450a9dfaf9febe + size: 22052 + timestamp: 1768574057200 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.10-pyhd8ed1ab_0.conda + sha256: 1ce33112e545bcfc41f92747f4f8b0f36310937339334bfaabe2496a65b3f5b2 + md5: 0fc7cfcd261709ef5591c1ea3415e8a7 depends: - async-lru >=1.0.0 - - httpx >=0.25.0 + - httpx >=0.25.0,<1 - importlib-metadata >=4.8.3 - - ipykernel >=6.5.0 + - ipykernel >=6.5.0,!=6.30.0 - jinja2 >=3.0.3 - jupyter-lsp >=2.0.0 - jupyter_core @@ -4369,7 +4202,7 @@ packages: - jupyterlab_server >=2.27.1,<3 - notebook-shim >=0.2 - packaging - - python >=3.9 + - python >=3.10 - setuptools >=41.1.0 - tomli >=1.2.2 - tornado >=6.2.0 @@ -4378,8 +4211,8 @@ packages: license_family: BSD purls: - pkg:pypi/jupyterlab?source=hash-mapping - size: 8236973 - timestamp: 1748273017680 + size: 8034306 + timestamp: 1761146412485 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 md5: fd312693df06da3578383232528c468d @@ -4394,27 +4227,25 @@ packages: - pkg:pypi/jupyterlab-pygments?source=hash-mapping size: 18711 timestamp: 1733328194037 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - sha256: d03d0b7e23fa56d322993bc9786b3a43b88ccc26e58b77c756619a921ab30e86 - md5: 9dc4b2b0f41f0de41d27f3293e319357 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + sha256: 381d2d6a259a3be5f38a69463e0f6c5dcf1844ae113058007b51c3bef13a7cee + md5: a63877cb23de826b1620d3adfccc4014 depends: - babel >=2.10 - - importlib-metadata >=4.8.3 - jinja2 >=3.0.3 - json5 >=0.9.0 - jsonschema >=4.18 - jupyter_server >=1.21,<3 - packaging >=21.3 - - python >=3.9 + - python >=3.10 - requests >=2.31 - constrains: - - openapi-core >=0.18.0,<0.19.0 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/jupyterlab-server?source=hash-mapping - size: 49449 - timestamp: 1733599666357 + size: 51621 + timestamp: 1761145478692 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlite-core-0.6.1-pyhe01879c_0.conda sha256: 8f4941c807040872c136f7a71e6edc4141e655ab2f662c3f58d2855c29303b8a md5: dec0e1c588f35be2f1da509adf115413 @@ -4444,83 +4275,84 @@ packages: - pkg:pypi/jupyterlite-pyodide-kernel?source=hash-mapping size: 369663 timestamp: 1749124243774 -- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb - md5: 30186d27e2c9fa62b45fb1476b7200e3 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 depends: - - libgcc-ng >=10.3.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 license: LGPL-2.1-or-later purls: [] - size: 117831 - timestamp: 1646151697040 -- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 - md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 + md5: fb53fb07ce46a575c5d004bbc96032c2 depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1370023 - timestamp: 1719463201255 -- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - sha256: 83b52685a4ce542772f0892a0f05764ac69d57187975579a0835ff255ae3ef9c - md5: d4765c524b1d91567886bde656fb514b + size: 1386730 + timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h207b36a_0.conda + sha256: df009385e8262c234c0dae9016540b86dad3d299f0d9366d08e327e8e7731634 + md5: e66e2c52d2fdddcf314ad750fb4ebb4a depends: - __osx >=10.13 - - libcxx >=16 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - openssl >=3.3.1,<4.0a0 + - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1185323 - timestamp: 1719463492984 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b - md5: c6dc8a0fdec13a0565936655c33069a1 + size: 1193620 + timestamp: 1769770267475 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + sha256: c0a0bf028fe7f3defcdcaa464e536cf1b202d07451e18ad83fdd169d15bef6ed + md5: e446e1822f4da8e5080a9de93474184d depends: - __osx >=11.0 - - libcxx >=16 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - openssl >=3.3.1,<4.0a0 + - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1155530 - timestamp: 1719463474401 -- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 - md5: 31aec030344e962fbd7dbbbbd68e60a9 + size: 1160828 + timestamp: 1769770119811 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + sha256: eb60f1ad8b597bcf95dee11bc11fe71a8325bc1204cf51d2bb1f2120ffd77761 + md5: 4432f52dc0c8eb6a7a6abc00a037d93c depends: - - openssl >=3.3.1,<4.0a0 + - openssl >=3.5.5,<4.0a0 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 712034 - timestamp: 1719463874284 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 - md5: 01f8d123c96816249efd255a31ad7712 + size: 751055 + timestamp: 1769769688841 +- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 + md5: 9b965c999135d43a3d0f7bd7d024e26a depends: - - __glibc >=2.17,<3.0.a0 - constrains: - - binutils_impl_linux-64 2.43 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 671240 - timestamp: 1740155456116 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/lark?source=hash-mapping + size: 94312 + timestamp: 1761596921009 - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c md5: 18335a698559cdbcd86150a48bf54ba6 @@ -4534,16 +4366,6 @@ packages: purls: [] size: 728002 timestamp: 1774197446916 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-20.1.1-hf95d169_0.conda - sha256: b30ef239517cfffb71d8ece7b903afe2a1bac0425f5bd38976b35d3cbf77312b - md5: 85cff0ed95d940c4762d5a99a6fe34ae - depends: - - __osx >=10.13 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 562132 - timestamp: 1742449741333 - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.4-h19cb2f5_0.conda sha256: 596a0bdd5321c5e41a4734f18b35bcbc5d116079d13bc40d765fd93c32b285d1 md5: 4394b1ba4b9604ac4e1c5bdc74451279 @@ -4554,16 +4376,6 @@ packages: purls: [] size: 567125 timestamp: 1776815441323 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.1-ha82da77_0.conda - sha256: 80dd8ae3fbcf508ed72f074ada2c7784298e822e8d19c3b84c266bb31456d77c - md5: 833c4899914bf96caf64b52ef415e319 - depends: - - __osx >=11.0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 561543 - timestamp: 1742449846779 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda sha256: 25a0d02148a39b665d9c2957676faf62a4d2a58494d53b201151199a197db4b0 md5: 448a1af83a9205655ee1cf48d3875ca3 @@ -4611,19 +4423,6 @@ packages: purls: [] size: 107691 timestamp: 1738479560845 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - sha256: 56541b98447b58e52d824bd59d6382d609e11de1f8adf20b23143e353d2b8d26 - md5: db833e03127376d461e1e13e76f09b6c - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - expat 2.6.4.* - license: MIT - license_family: MIT - purls: [] - size: 73304 - timestamp: 1730967041968 - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12 md5: a3b390520c563d78cc58974de95a03e5 @@ -4637,18 +4436,6 @@ packages: purls: [] size: 77241 timestamp: 1777846112704 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.4-h240833e_0.conda - sha256: d10f43d0c5df6c8cf55259bce0fe14d2377eed625956cddce06f58827d288c59 - md5: 20307f4049a735a78a29073be1be2626 - depends: - - __osx >=10.13 - constrains: - - expat 2.6.4.* - license: MIT - license_family: MIT - purls: [] - size: 70758 - timestamp: 1730967204736 - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda sha256: 5ebcc413d0a75da926a8b9b681d7d12c9562993991ba49c90a9881c4a59bdc11 md5: d2e01f78c1daaeb4d2aa870125ebcd7e @@ -4661,18 +4448,6 @@ packages: purls: [] size: 75242 timestamp: 1777846416221 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 - md5: 38d2656dd914feb0cab8c629370768bf - depends: - - __osx >=11.0 - constrains: - - expat 2.6.4.* - license: MIT - license_family: MIT - purls: [] - size: 64693 - timestamp: 1730967175868 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda sha256: f4b1cafc59afaede8fa0a2d9cf376840f1c553001acd72f6ead18bbc8ac8c49c md5: 65466e82c09e888ca7560c11a97d5450 @@ -4685,20 +4460,6 @@ packages: purls: [] size: 68789 timestamp: 1777846180142 -- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda - sha256: 0c0447bf20d1013d5603499de93a16b6faa92d7ead870d96305c0f065b6a5a12 - md5: eb383771c680aa792feb529eaf9df82f - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - expat 2.6.4.* - license: MIT - license_family: MIT - purls: [] - size: 139068 - timestamp: 1730967442102 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 md5: 264e350e035092b5135a2147c238aec4 @@ -4713,17 +4474,6 @@ packages: purls: [] size: 71094 timestamp: 1777846223617 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - sha256: 67a6c95e33ebc763c1adc3455b9a9ecde901850eb2fceb8e646cc05ef3a663da - md5: e3eb7806380bc8bcecba6d749ad5f026 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 53415 - timestamp: 1739260413716 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 md5: a360c33a5abe61c07959e449fa1453eb @@ -4735,16 +4485,6 @@ packages: purls: [] size: 58592 timestamp: 1769456073053 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_0.conda - sha256: 7805fdc536a3da7fb63dc48e040105cd4260c69a1d2bf5804dadd31bde8bab51 - md5: b8667b0d0400b8dcb6844d8e06b2027d - depends: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: [] - size: 47258 - timestamp: 1739260651925 - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda sha256: 951958d1792238006fdc6fce7f71f1b559534743b26cc1333497d46e5903a2d6 md5: 66a0dc7464927d0853b590b6f53ba3ea @@ -4755,14 +4495,6 @@ packages: purls: [] size: 53583 timestamp: 1769456300951 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca - md5: 086914b672be056eb70fd4285b6783b6 - license: MIT - license_family: MIT - purls: [] - size: 39020 - timestamp: 1636488587153 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 md5: 43c04d9cb46ef176bb2a4c77e324d599 @@ -4773,18 +4505,6 @@ packages: purls: [] size: 40979 timestamp: 1769456747661 -- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_0.conda - sha256: 77922d8dd2faf88ac6accaeebf06409d1820486fde710cff6b554d12273e46be - md5: 31d5107f75b2f204937728417e2e39e5 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: [] - size: 40830 - timestamp: 1739260917585 - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 md5: 720b39f5ec0610457b725eb3f396219a @@ -4797,20 +4517,6 @@ packages: purls: [] size: 45831 timestamp: 1769456418774 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 - md5: ef504d1acbd74b7cc6849ef8af47dd03 - depends: - - __glibc >=2.17,<3.0.a0 - - _openmp_mutex >=4.5 - constrains: - - libgomp 14.2.0 h767d61c_2 - - libgcc-ng ==14.2.0=*_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 847885 - timestamp: 1740240653082 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 md5: 0aa00f03f9e39fb9876085dee11a85d4 @@ -4825,26 +4531,16 @@ packages: purls: [] size: 1041788 timestamp: 1771378212382 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 - md5: a2222a6ada71fb478682efe483ce0f92 - depends: - - libgcc 14.2.0 h767d61c_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 53758 - timestamp: 1740240660904 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e - md5: 06d02030237f4d5b3d9a7e7d348fe3c6 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + sha256: e318a711400f536c81123e753d4c797a821021fb38970cebfb3f454126016893 + md5: d5e96b1ed75ca01906b3d2469b4ce493 depends: - - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 459862 - timestamp: 1740240588123 + size: 27526 + timestamp: 1771378224552 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 md5: 239c5e9546c38a1e884d69effcf4c882 @@ -4865,25 +4561,6 @@ packages: purls: [] size: 790176 timestamp: 1754908768807 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - sha256: 18a4afe14f731bfb9cf388659994263904d20111e42f841e9eea1bb6f91f4ab4 - md5: e796ff8ddc598affdf7c173d6145f087 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-only - purls: [] - size: 713084 - timestamp: 1740128065462 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h4b5e92a_1.conda - sha256: c2a9c65a245c7bcb8c17c94dd716dad2d42b7c98e0c17cc5553a5c60242c4dda - md5: 6283140d7b2b55b6b095af939b71b13f - depends: - - __osx >=10.13 - license: LGPL-2.1-only - purls: [] - size: 669052 - timestamp: 1740128415026 - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda sha256: a1c8cecdf9966921e13f0ae921309a1f415dfbd2b791f2117cf7e8f5e61a48b6 md5: 210a85a1119f97ea7887188d176db135 @@ -4902,26 +4579,6 @@ packages: purls: [] size: 750379 timestamp: 1754909073836 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda - sha256: d30780d24bf3a30b4f116fca74dedb4199b34d500fe6c52cced5f8cc1e926f03 - md5: 450e6bdc0c7d986acf7b8443dce87111 - depends: - - __osx >=11.0 - license: LGPL-2.1-only - purls: [] - size: 681804 - timestamp: 1740128227484 -- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda - sha256: ea5ed2b362b6dbc4ba7188eb4eaf576146e3dfc6f4395e9f0db76ad77465f786 - md5: 21fc5dba2cbcd8e5e26ff976a312122c - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: LGPL-2.1-only - purls: [] - size: 638142 - timestamp: 1740128665984 - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 md5: 64571d1dd6cdcfa25d0664a5950fdaa2 @@ -4933,16 +4590,6 @@ packages: purls: [] size: 696926 timestamp: 1754909290005 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f - md5: 42d5b6a0f30d3c10cd88cb8584fda1cb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: 0BSD - purls: [] - size: 111357 - timestamp: 1738525339684 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d md5: b88d90cad08e6bc8ad540cb310a761fb @@ -4955,15 +4602,6 @@ packages: purls: [] size: 113478 timestamp: 1775825492909 -- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.6.4-hd471939_0.conda - sha256: a895b5b16468a6ed436f022d72ee52a657f9b58214b91fabfab6230e3592a6dd - md5: db9d7b0152613f097cdb61ccf9f70ef5 - depends: - - __osx >=10.13 - license: 0BSD - purls: [] - size: 103749 - timestamp: 1738525448522 - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda sha256: d9e2006051529aec5578c6efeb13bb6a7200a014b2d5a77a579e83a8049d5f3c md5: becdfbfe7049fa248e52aa37a9df09e2 @@ -4975,15 +4613,6 @@ packages: purls: [] size: 105724 timestamp: 1775826029494 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - sha256: 560c59d3834cc652a84fb45531bd335ad06e271b34ebc216e380a89798fe8e2c - md5: e3fd1f8320a100f2b210e690a57cd615 - depends: - - __osx >=11.0 - license: 0BSD - purls: [] - size: 98945 - timestamp: 1738525462560 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e md5: b1fd823b5ae54fbec272cea0811bd8a9 @@ -4995,17 +4624,6 @@ packages: purls: [] size: 92472 timestamp: 1775825802659 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda - sha256: 3f552b0bdefdd1459ffc827ea3bf70a6a6920c7879d22b6bfd0d73015b55227b - md5: c48f6ad0ef0a555b27b233dfcab46a90 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: 0BSD - purls: [] - size: 104465 - timestamp: 1738525557254 - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 md5: 8f83619ab1588b98dd99c90b0bfc5c6d @@ -5019,17 +4637,6 @@ packages: purls: [] size: 106486 timestamp: 1775825663227 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - sha256: d02d1d3304ecaf5c728e515eb7416517a0b118200cd5eacbe829c432d1664070 - md5: aeb98fdeb2e8f25d43ef71fbacbeec80 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 89991 - timestamp: 1723817448345 - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 md5: 2c21e66f50753a083cbe6b80f38268fa @@ -5051,16 +4658,6 @@ packages: purls: [] size: 79899 timestamp: 1769482558610 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hfdf4475_0.conda - sha256: 791be3d30d8e37ec49bcc23eb8f1e1415d911a7c023fa93685f2ea485179e258 - md5: ed625b2e59dff82859c23dd24774156b - depends: - - __osx >=10.13 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 76561 - timestamp: 1723817691512 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 md5: 57c4be259f5e0b99a5983799a228ae55 @@ -5071,28 +4668,6 @@ packages: purls: [] size: 73690 timestamp: 1769482560514 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h99b78c6_0.conda - sha256: f7917de9117d3a5fe12a39e185c7ce424f8d5010a6f97b4333e8a1dcb2889d16 - md5: 7476305c35dd9acef48da8f754eedb40 - depends: - - __osx >=11.0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 69263 - timestamp: 1723817629767 -- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - sha256: fc529fc82c7caf51202cc5cec5bb1c2e8d90edbac6d0a4602c966366efe3c7bf - md5: 74860100b2029e2523cf480804c76b9b - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 88657 - timestamp: 1723861474602 - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda sha256: 40dcd0b9522a6e0af72a9db0ced619176e7cfdb114855c7a64f278e73f8a7514 md5: e4a9fc2bba3b022dad998c78856afe47 @@ -5105,65 +4680,56 @@ packages: purls: [] size: 89411 timestamp: 1769482314283 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 - md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 depends: - - libgcc-ng >=12 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 license: LGPL-2.1-only license_family: GPL purls: [] - size: 33408 - timestamp: 1697359010159 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 - md5: a587892d3c13b6621a6091be690dbca2 + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + sha256: 64e5c80cbce4680a2d25179949739a6def695d72c40ca28f010711764e372d97 + md5: 7af961ef4aa2c1136e11dd43ded245ab depends: - - libgcc-ng >=12 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 license: ISC purls: [] - size: 205978 - timestamp: 1716828628198 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.20-hfdf4475_0.conda - sha256: d3975cfe60e81072666da8c76b993af018cf2e73fe55acba2b5ba0928efaccf5 - md5: 6af4b059e26492da6013e79cbcb4d069 + size: 277661 + timestamp: 1772479381288 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.21-hc6ced15_3.conda + sha256: 7dd254e844372fbf3a60a7c029df1ea0cb3fa0b18586cda769d9cd6cc0e59c4b + md5: c4b8a6c8a8aa6ed657a3c1c1eb6917e9 depends: - __osx >=10.13 license: ISC purls: [] - size: 210249 - timestamp: 1716828641383 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - sha256: fade8223e1e1004367d7101dd17261003b60aa576df6d7802191f8972f7470b1 - md5: a7ce36e284c5faaf93c220dfc39e3abd + size: 291865 + timestamp: 1772479644707 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.21-h1a92334_3.conda + sha256: df603472ea1ebd8e7d4fb71e4360fe48d10b11c240df51c129de1da2ff9e8227 + md5: 7cc5247987e6d115134ebab15186bc13 depends: - __osx >=11.0 license: ISC purls: [] - size: 164972 - timestamp: 1716828607917 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - sha256: 7bcb3edccea30f711b6be9601e083ecf4f435b9407d70fc48fbcf9e5d69a0fc6 - md5: 198bb594f202b205c7d18b936fa4524f + size: 248039 + timestamp: 1772479570912 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.21-h6a83c73_3.conda + sha256: d915f4fa8ebbf237c7a6e511ed458f2cfdc7c76843a924740318a15d0dd33d6d + md5: da2aa614d16a795b3007b6f4a1318a81 depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 license: ISC purls: [] - size: 202344 - timestamp: 1716828757533 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - sha256: a086289bf75c33adc1daed3f1422024504ffb5c3c8b3285c49f025c29708ed16 - md5: 962d6ac93c30b1dfc54c9cccafd1003e - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: Unlicense - purls: [] - size: 918664 - timestamp: 1742083674731 + size: 276860 + timestamp: 1772479407566 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda sha256: ec37c79f737933bbac965f5dc0f08ef2790247129a84bb3114fad4900adce401 md5: 810d83373448da85c3f673fbcb7ad3a3 @@ -5176,16 +4742,16 @@ packages: purls: [] size: 958864 timestamp: 1775753750179 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.49.1-hdb6dae5_2.conda - sha256: 82695c9b16a702de615c8303387384c6ec5cf8b98e16458e5b1935b950e4ec38 - md5: 1819e770584a7e83a81541d8253cbabe +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h77d7759_0.conda + sha256: 0dd0e92a2dc2c9978b7088c097fb078caefdd44fb8e24e3327d16c6a120378f7 + md5: 19915aab82b4593237be8ef977aad29e depends: - - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - license: Unlicense + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 977701 - timestamp: 1742083869897 + size: 1002564 + timestamp: 1775754043809 - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.0-h8f8c405_0.conda sha256: ae9d83cab8988a7d4ccec411fef23c141b5b3d301db3e926ab7cd4befe3764e6 md5: f2bb6692dfb33a1bbce746aa812a9a5b @@ -5197,16 +4763,6 @@ packages: purls: [] size: 1007272 timestamp: 1775754456682 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.49.1-h3f77e49_2.conda - sha256: 907a95f73623c343fc14785cbfefcb7a6b4f2bcf9294fcb295c121611c3a590d - md5: 3b1e330d775170ac46dff9a94c253bd0 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: Unlicense - purls: [] - size: 900188 - timestamp: 1742083865246 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.0-h1b79a29_0.conda sha256: 1a9d1e3e18dbb0b87cff3b40c3e42703730d7ac7ee9b9322c2682196a81ba0c3 md5: 8423c008105df35485e184066cad4566 @@ -5217,17 +4773,6 @@ packages: purls: [] size: 920039 timestamp: 1775754485962 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.49.1-h67fdade_2.conda - sha256: c092d42d00fd85cf609cc58574ba2b03c141af5762283f36f5dd445ef7c0f4fe - md5: b58b66d4ad1aaf1c2543cbbd6afb1a59 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Unlicense - purls: [] - size: 1081292 - timestamp: 1742083956001 - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda sha256: 7a6256ea136936df4c4f3b227ba1e273b7d61152f9811b52157af497f07640b0 md5: 4152b5a8d2513fd7ae9fb9f221a5595d @@ -5239,50 +4784,19 @@ packages: purls: [] size: 1301855 timestamp: 1775753831574 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - sha256: 8f5bd92e4a24e1d35ba015c5252e8f818898478cb3bc50bd8b12ab54707dc4da - md5: a78c856b6dc6bf4ea8daeb9beaaa3fb0 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc 14.2.0 h767d61c_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 3884556 - timestamp: 1740240685253 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e md5: 1b08cd684f34175e4514474793d44bcb depends: - __glibc >=2.17,<3.0.a0 - libgcc 15.2.0 he0feb66_18 - constrains: - - libstdcxx-ng ==15.2.0=*_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 5852330 - timestamp: 1771378262446 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda - sha256: e86f38b007cf97cc2c67cd519f2de12a313c4ee3f5ef11652ad08932a5e34189 - md5: c75da67f045c2627f59e6fcb5f4e3a9b - depends: - - libstdcxx 14.2.0 h8f9b012_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 53830 - timestamp: 1740240722530 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 - md5: 40b61aab5c7ba9ff276c41cfffe6b80b - depends: - - libgcc-ng >=12 - license: BSD-3-Clause - license_family: BSD + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 33601 - timestamp: 1680112270483 + size: 5852330 + timestamp: 1771378262446 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 md5: 38ffe67b78c9d4de527be8315e5ada2c @@ -5303,22 +4817,6 @@ packages: purls: [] size: 100393 timestamp: 1702724383534 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.7-h0d44e9d_0.conda - sha256: d19b28caa42ac9c5ac2aa73f5f44947f78ab416467dc40926484f3afbcc31ed1 - md5: 3ac6daa5c1210293a6deaec0c345b230 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.6.4,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - constrains: - - icu <0.0a0 - license: MIT - license_family: MIT - purls: [] - size: 689316 - timestamp: 1743091137869 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda sha256: 3bc5551720c58591f6ea1146f7d1539c734ed1c40e7b9f5cb8cb7e900c509aba md5: 995d8c8bad2a3cc8db14675a153dec2b @@ -5335,20 +4833,22 @@ packages: purls: [] size: 46810 timestamp: 1776376751152 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.7-hebb159f_0.conda - sha256: 21119df0a2267a9fc52d67bdf55e5449a2cdcc799865e2f90ab734fd61234ed8 - md5: 45786cf4067df4fbe9faf3d1c25d3acf +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h0712280_0.conda + sha256: caf6e73fa53c3dec3227ea67de231b0f7009544252684e816c6f2f414aeb55c9 + md5: 81ac54c8b2eb51fceb94eb0817b93cf3 depends: - - __osx >=10.13 - - icu >=75.1,<76.0a0 + - __osx >=11.0 - libiconv >=1.18,<2.0a0 - - liblzma >=5.6.4,<6.0a0 - - libzlib >=1.3.1,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.3 h0d7f165_0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - icu <0.0a0 license: MIT license_family: MIT purls: [] - size: 609769 - timestamp: 1743091248758 + size: 40803 + timestamp: 1776377589058 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda sha256: 24248928e63b5de45012c8ad3fd6b350ae1fe2fc355613bb89ee5f0a35835bea md5: 33f30d4878d1f047da82a669c33b307d @@ -5364,20 +4864,6 @@ packages: purls: [] size: 40836 timestamp: 1776377277986 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.7-h178c5d8_0.conda - sha256: d3ddc9ae8a5474f16f213ca41b3eda394e1eb1253f3ac85d3c6c99adcfb226d8 - md5: aa838a099ba09429cb80cc876b032ac4 - depends: - - __osx >=11.0 - - icu >=75.1,<76.0a0 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.6.4,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT - purls: [] - size: 582736 - timestamp: 1743091513375 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.3-heed7d32_0.conda sha256: 4d9c117b2dd222cf891710d5f6a570ebb275479979843a1477ac54ed50907b40 md5: 0c1fdc80534d8f25fd74722aba81f044 @@ -5394,20 +4880,6 @@ packages: purls: [] size: 41663 timestamp: 1776377341241 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.7-he286e8c_0.conda - sha256: 99182f93f1e7b678534df5f07ff94d7bf13a51386050f8fa9411fec764d0f39f - md5: aec4cf455e4c6cc2644abb348de7ff20 - depends: - - libiconv >=1.18,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: [] - size: 1513490 - timestamp: 1743091551681 - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda sha256: da68af9d9d28d65a6916db1bef68f8a25c64c4fdcf759f32a2d2f2f143220adf md5: e3b5acbb857a12f5d59e8d174bc536c0 @@ -5443,6 +4915,22 @@ packages: purls: [] size: 559775 timestamp: 1776376739004 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h0d7f165_0.conda + sha256: daa69a1dd887b2dbac44327bc5af73a4d41fa63bc6dc609782fdda9aec187895 + md5: 5eb194ed01ed3f5a64b6fcec1b399d96 + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - icu <0.0a0 + - libxml2 2.15.3 + license: MIT + license_family: MIT + purls: [] + size: 495267 + timestamp: 1776377547505 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda sha256: 437f003e299d77403db42d17e532d686236f357ac5c3d6bf466558c697902597 md5: c74ae93cd7876e3a9c4b5569d5e29e34 @@ -5493,17 +4981,6 @@ packages: purls: [] size: 518869 timestamp: 1776376971242 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda - sha256: 684e9b67ef7b9ca0ca993762eeb39705ec58e2e7f958555c758da7ef416db9f3 - md5: e71f31f8cfb0a91439f2086fc8aa0461 - depends: - - libgcc-ng >=12 - - libxml2 >=2.12.1,<3.0.0a0 - license: MIT - license_family: MIT - purls: [] - size: 254297 - timestamp: 1701628814990 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda sha256: 0694760a3e62bdc659d90a14ae9c6e132b525a7900e59785b18a08bb52a5d7e5 md5: 87e6096ec6d542d1c1f8b33245fe8300 @@ -5517,16 +4994,6 @@ packages: purls: [] size: 245434 timestamp: 1757963724977 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.39-h03b04e6_0.conda - sha256: decfc5614a10231a17543b7366616fb2d88c14be6dd9dd5ecde63aa9a5acfb9e - md5: a6e0cec6b3517ffc6b5d36a920fc9312 - depends: - - libxml2 >=2.12.1,<3.0.0a0 - license: MIT - license_family: MIT - purls: [] - size: 231368 - timestamp: 1701628933115 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda sha256: 00d6b5e92fc1c5d86e095b9b6840f793d9fc4c9b4a7753fa0f8197ab11d5eb90 md5: 367b8029352f3899fb76cc20f4d144b9 @@ -5539,16 +5006,6 @@ packages: purls: [] size: 225660 timestamp: 1757964032926 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.39-h223e5b9_0.conda - sha256: 2f1d99ef3fb960f23a63f06cf65ee621a5594a8b4616f35d9805be44617a92af - md5: 560c9cacc33e927f55b998eaa0cb1732 - depends: - - libxml2 >=2.12.1,<3.0.0a0 - license: MIT - license_family: MIT - purls: [] - size: 225705 - timestamp: 1701628966565 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda sha256: 7a4d0676ab1407fecb24d4ada7fe31a98c8889f61f04612ea533599c22b8c472 md5: 90f7ed12bb3c164c758131b3d3c2ab0c @@ -5561,19 +5018,6 @@ packages: purls: [] size: 220345 timestamp: 1757964000982 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda - sha256: 6e3d99466d2076c35e7ac8dcdfe604da3d593f55b74a5b8e96c2b2ff63c247aa - md5: 279ee338c9b34871d578cb3c7aa68f70 - depends: - - libxml2 >=2.12.1,<3.0.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: [] - size: 418542 - timestamp: 1701629338549 - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda sha256: 13da38939c2c20e7112d683ab6c9f304bfaf06230a2c6a7cf00359da1a003ec7 md5: 46034d9d983edc21e84c0b36f1b4ba61 @@ -5588,19 +5032,6 @@ packages: purls: [] size: 420223 timestamp: 1757963935611 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other - purls: [] - size: 60963 - timestamp: 1727963148474 - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 md5: d87ff7921124eccd67248aa483c23fec @@ -5613,18 +5044,6 @@ packages: purls: [] size: 63629 timestamp: 1774072609062 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - sha256: 8412f96504fc5993a63edf1e211d042a1fd5b1d51dedec755d2058948fcced09 - md5: 003a54a4e32b02f7355b50a837e699da - depends: - - __osx >=10.13 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other - purls: [] - size: 57133 - timestamp: 1727963183990 - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda sha256: 4c6da089952b2d70150c74234679d6f7ac04f4a98f9432dec724968f912691e7 md5: 30439ff30578e504ee5e0b390afc8c65 @@ -5637,18 +5056,6 @@ packages: purls: [] size: 59000 timestamp: 1774073052242 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b - md5: 369964e85dc26bfe78f41399b366c435 - depends: - - __osx >=11.0 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other - purls: [] - size: 46438 - timestamp: 1727963202283 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 md5: bc5a5721b6439f2f62a84f2548136082 @@ -5661,20 +5068,6 @@ packages: purls: [] size: 47759 timestamp: 1774072956767 -- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 - md5: 41fbfac52c601159df6c01f875de31b9 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other - purls: [] - size: 55476 - timestamp: 1727963768015 - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 md5: dbabbd6234dea34040e631f87676292f @@ -5689,38 +5082,40 @@ packages: purls: [] size: 58347 timestamp: 1774072851498 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py312he28fd5a_0.conda - sha256: 4f3a78b59890f2175a381d9ae5e74b4523aea23daaa01cafbb150456bc8b857c - md5: 52d16dd592060d4b2fa9ad325e0c1f90 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda + sha256: cb02acf7254cc5d2dff65fb93c52103d4858da391a13cf4f24b49360ac74da64 + md5: b19cc6de7dc3b8c6b5996384dfc163e2 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - libgcc >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: BSD-3-Clause and MIT-CMU purls: - - pkg:pypi/lxml?source=hash-mapping - size: 1403423 - timestamp: 1739211901003 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-5.3.1-py313h6eb7059_0.conda - sha256: 6dc1193e22bfe156dd7d392ada5947b56e5c26866e864e00372188d4329f9c53 - md5: eae971feba43f66f4372e98a82d9b576 + - pkg:pypi/lxml?source=compressed-mapping + size: 1575644 + timestamp: 1776512598986 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py313h4a16004_0.conda + sha256: 96dc58adadf29655c0a010c5268ef7cf3c7fb676015b7c96bc9dc01c800b659e + md5: e5ba7f288d2389e3801952efa1be111f depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - libgcc >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 license: BSD-3-Clause and MIT-CMU purls: - pkg:pypi/lxml?source=hash-mapping - size: 1404411 - timestamp: 1739211853813 + size: 1572226 + timestamp: 1776512601322 - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py314hae3bed6_0.conda sha256: 8edbaad598410cd3a9b69e94281eaa5f8632585c882618e61a690975031e4a25 md5: 8115b838f94eac2cef6bff236c4d25f4 @@ -5738,36 +5133,38 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1581460 timestamp: 1776512598345 -- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py312h91b2f42_0.conda - sha256: 9d8caf0b13e42a214f47f21e4a4696a7de37e81b182fb88c0e922b5940fb716e - md5: a1b3a0206fc6c434fadc25d818002b82 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda + sha256: c60c2d0dd8637bd7569c16d2db80396d96951d939ca519b7284106dce6607656 + md5: 10afd3ca188254fb64eb1be0ae4d3368 depends: - - __osx >=10.13 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: BSD-3-Clause and MIT-CMU purls: - pkg:pypi/lxml?source=hash-mapping - size: 1236870 - timestamp: 1739212062656 -- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-5.3.1-py313he540dec_0.conda - sha256: 063fffab0b43e556e849dee6cd6caa9a5977fa6478d422c8570bfd2008349434 - md5: 27f9a6405ff10148525d7127aca40656 + size: 1394606 + timestamp: 1776513040440 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py313hdc5d0a5_0.conda + sha256: 01fe4c1673a0f99e53a28ddef1ee4fac4c5e7ab0c3691287edd2d7013cecfc01 + md5: 25a36c8381c419eb1bd90c38a35ff375 depends: - - __osx >=10.13 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - __osx >=11.0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 license: BSD-3-Clause and MIT-CMU purls: - pkg:pypi/lxml?source=hash-mapping - size: 1258398 - timestamp: 1739212205592 + size: 1412715 + timestamp: 1776512943610 - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py314h1d4708b_0.conda sha256: 9f47ec5a688eef6a645853b218dc653a2e969f22368498379b93c263635ec69d md5: 29604e0a4d440ed409009b8aaad74e59 @@ -5784,38 +5181,40 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1417422 timestamp: 1776512991743 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py312h9535dd2_0.conda - sha256: b899871ecf3f331e3047295897809758a02a144e4118f1378ca443c62772cd2c - md5: f9d4307bbe7d394ac3634fe85a4c0e94 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda + sha256: 63f161632c93b16f98cf770744151013f915911adea78f59e32bfe49e25a7578 + md5: 775a0f5794a44bbf310e1c3e458c0ffd depends: - __osx >=11.0 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 license: BSD-3-Clause and MIT-CMU purls: - - pkg:pypi/lxml?source=hash-mapping - size: 1200955 - timestamp: 1739212041952 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-5.3.1-py313hbcba52a_0.conda - sha256: 096ad08d5a0b84d0849022431867a3dcff7ef92405155146dd116d94443fbbcc - md5: ec73a38e091a36c68cfc4fa790778c02 + - pkg:pypi/lxml?source=compressed-mapping + size: 1349054 + timestamp: 1776512963897 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py313h28ec6f0_0.conda + sha256: caabfe28000b4242a33c46e0625e3c72fa94e5246926d41386f896a26cc91ad8 + md5: 15ee46eb46f3a43538ecc9236ac399ee depends: - __osx >=11.0 - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 license: BSD-3-Clause and MIT-CMU purls: - pkg:pypi/lxml?source=hash-mapping - size: 1219666 - timestamp: 1739211889959 + size: 1367799 + timestamp: 1776513002402 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py314h264e108_0.conda sha256: 85e3c1874326e4681d5dcfc48273fcfcd653b6f3681d7e15e0890e69e01db0f5 md5: c11f8cea755d00e8389aebdc069156bd @@ -5833,40 +5232,42 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1375410 timestamp: 1776512951800 -- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py312h53bce91_0.conda - sha256: 78519f3a92e8e284792b9b13d4240643b47b3c1902b2288e2a4dfeb83f78e787 - md5: c86f153c26b4d6235de9e19eafc01ce8 +- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py312h2f35c63_0.conda + sha256: 7c2136a04c28f557618b37c2e89060b6981eb2db30b8388755424d951e1f25dd + md5: 53c7c8254c634fafe17c04e19e182924 depends: - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: BSD-3-Clause and MIT-CMU purls: - pkg:pypi/lxml?source=hash-mapping - size: 1045295 - timestamp: 1739212451593 -- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-5.3.1-py313hd92aa1b_0.conda - sha256: 08590d6e2d87152d445bfe497f03ce067d7fb6f60c3572e0f8cbbe6bb134e100 - md5: 5c5869e0fbee405db8ec5c225c302f9c - depends: - - libxml2 >=2.13.5,<3.0a0 - - libxslt >=1.1.39,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + size: 1234704 + timestamp: 1776512727496 +- conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py313h1af1686_0.conda + sha256: 5e3de3d6615d45f5700bb1a78914808897b526f51809909a9ed4e9b53281baac + md5: 74357aaf2212558ff539c41fd71b9f20 + depends: + - libxml2 + - libxml2-16 >=2.14.6 + - libxslt >=1.1.43,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: BSD-3-Clause and MIT-CMU purls: - pkg:pypi/lxml?source=hash-mapping - size: 1051362 - timestamp: 1739212280294 + size: 1240348 + timestamp: 1776512726179 - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.0-py314hcdb55d9_0.conda sha256: b6ac0a0ef4da0cdfaff9d1204160e2e88d57fe9d884ff2491bb5ef78fba9cd02 md5: 0171707b7a0f89c0daa9e75d39ce0ca8 @@ -5885,18 +5286,6 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1239608 timestamp: 1776512723280 -- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - sha256: 0fbacdfb31e55964152b24d5567e9a9996e1e7902fb08eb7d91b5fd6ce60803a - md5: fee3164ac23dfca50cfcc8b85ddefb81 - depends: - - mdurl >=0.1,<1 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/markdown-it-py?source=hash-mapping - size: 64430 - timestamp: 1733250550053 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda sha256: 7b1da4b5c40385791dbc3cc85ceea9fad5da680a27d5d3cb8bfaa185e304a89e md5: 5b5203189eb668f042ac2b0826244964 @@ -5909,12 +5298,12 @@ packages: - pkg:pypi/markdown-it-py?source=hash-mapping size: 64736 timestamp: 1754951288511 -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda - sha256: 4a6bf68d2a2b669fecc9a4a009abd1cf8e72c2289522ff00d81b5a6e51ae78f5 - md5: eb227c3e0bf58f5bd69c0532b157975b +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda + sha256: 5f3aad1f3a685ed0b591faad335957dbdb1b73abfd6fc731a0d42718e0653b33 + md5: 93a4752d42b12943a355b682ee43285b depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 constrains: @@ -5923,14 +5312,14 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 24604 - timestamp: 1733219911494 -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_1.conda - sha256: d812caf52efcea7c9fd0eafb21d45dadfd0516812f667b928bee50e87634fae5 - md5: 21b62c55924f01b6eef6827167b46acb + size: 26057 + timestamp: 1772445297924 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + sha256: 72ed7c0216541d65a17b171bf2eec4a3b81e9158d8ed48e59e1ecd3ae302d263 + md5: aeb9b9da79fd0258b3db091d1fefcd71 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 constrains: @@ -5939,8 +5328,8 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 24856 - timestamp: 1733219782830 + size: 26100 + timestamp: 1772445154165 - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda sha256: c279be85b59a62d5c52f5dd9a4cd43ebd08933809a8416c22c3131595607d4cf md5: 9a17c4307d23318476d7fbf0fedc0cde @@ -5957,11 +5346,11 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 27424 timestamp: 1772445227915 -- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py312h3520af0_1.conda - sha256: d521e272f7789ca62e7617058a4ea3bd79efa73de1a39732df209ca5299e64e2 - md5: 32d6bc2407685d7e2d8db424f42018c6 +- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py312heb39f77_1.conda + sha256: 0eb418d4776a1a54c1869b11a5c4ae096ef9a46c8d7e481e32fa814561c5cfed + md5: d596f9d03043acd4ec711c844060da59 depends: - - __osx >=10.13 + - __osx >=11.0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 constrains: @@ -5969,14 +5358,14 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 23888 - timestamp: 1733219886634 -- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py313h717bdf5_1.conda - sha256: 297242943522a907c270bc2f191d16142707d970541b9a093640801b767d7aa7 - md5: a6fbde71416d6eb9898fcabf505a85c5 + - pkg:pypi/markupsafe?source=compressed-mapping + size: 25095 + timestamp: 1772445399364 +- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py313h035b7d0_1.conda + sha256: e589b345402e352fb47394f7bc311c241f37627a34a9becc9299b395809a5853 + md5: 3d88718cbd26857fb68fa899e80177ea depends: - - __osx >=10.13 + - __osx >=11.0 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 constrains: @@ -5985,8 +5374,8 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 24363 - timestamp: 1733219815199 + size: 25312 + timestamp: 1772445439146 - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py314h77fa6c7_1.conda sha256: 74507b481299c3d35dc7d1c35f9c92e2e94e0eda819b264f5f25b7552f8a7d64 md5: 5d45a74270e21481797387a209b3dec3 @@ -6002,9 +5391,9 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 26740 timestamp: 1772445674690 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda - sha256: 4aa997b244014d3707eeef54ab0ee497d12c0d0d184018960cce096169758283 - md5: 46e547061080fddf9cf95a0327e8aba6 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda + sha256: 330394fb9140995b29ae215a19fad46fcc6691bdd1b7654513d55a19aaa091c1 + md5: 11d95ab83ef0a82cc2de12c1e0b47fe4 depends: - __osx >=11.0 - python >=3.12,<3.13.0a0 @@ -6016,11 +5405,11 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 24048 - timestamp: 1733219945697 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py313ha9b7d5b_1.conda - sha256: 81759af8a9872c8926af3aa59dc4986eee90a0956d1ec820b42ac4f949a71211 - md5: 3acf05d8e42ff0d99820d2d889776fff + size: 25564 + timestamp: 1772445846939 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h65a2061_1.conda + sha256: f62892a42948c61aa0a13d9a36ff811651f0a1102331223594aecf3cc042bece + md5: 0195d558b0c0ab8f4af3089af83067c5 depends: - __osx >=11.0 - python >=3.13,<3.14.0a0 @@ -6032,8 +5421,8 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 24757 - timestamp: 1733219916634 + size: 26009 + timestamp: 1772445537524 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py314h6e9b3f0_1.conda sha256: 411153d14ee0d98be6e3751cf5cc0502db17bce2deebebb8779e33d29d0e525f md5: d33c0a15882b70255abdd54711b06a45 @@ -6050,40 +5439,40 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 27256 timestamp: 1772445397216 -- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda - sha256: bbb9595fe72231a8fbc8909cfa479af93741ecd2d28dfe37f8f205fef5df2217 - md5: 944fdd848abfbd6929e57c790b8174dd +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda + sha256: b744287a780211ac4595126ef96a44309c791f155d4724021ef99092bae4aace + md5: a73298d225c7852f97403ca105d10a13 depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 27582 - timestamp: 1733220007802 -- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py313hb4c8b1a_1.conda - sha256: f16cb398915f52d582bcea69a16cf69a56dab6ea2fab6f069da9c2c10f09534c - md5: ec9ecf6ee4cceb73a0c9a8cdfdf58bed + - pkg:pypi/markupsafe?source=compressed-mapping + size: 28510 + timestamp: 1772445175216 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_1.conda + sha256: 9dc626b6c00bc2dbd2494df689876ff675b93d92636ba5df8e37b99040a1f6bc + md5: 5cc690ddf943700e0ef50a265df31f03 depends: - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 27930 - timestamp: 1733220059655 + size: 28992 + timestamp: 1772445161959 - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py314h2359020_1.conda sha256: 02805a0f3cd168dbf13afc5e4aed75cc00fe538ce143527a6471485b36f5887c md5: 8de7b40f8b30a8fcaa423c2537fe4199 @@ -6101,30 +5490,18 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 30022 timestamp: 1772445159549 -- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - sha256: 69b7dc7131703d3d60da9b0faa6dd8acbf6f6c396224cf6aef3e855b8c0c41c6 - md5: af6ab708897df59bd6e7283ceab1b56b +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + sha256: 9d690334de0cd1d22c51bc28420663f4277cfa60d34fa5cad1ce284a13f1d603 + md5: 00e120ce3e40bad7bfc78861ce3c4a25 depends: - - python >=3.9 + - python >=3.10 - traitlets license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/matplotlib-inline?source=hash-mapping - size: 14467 - timestamp: 1733417051523 -- conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - sha256: c63ed79d9745109c0a70397713b0c07f06e7d3561abcb122cfc80a141ab3b449 - md5: af2060041d4f3250a7eb6ab3ec0e549b - depends: - - markdown-it-py >=1.0.0,<4.0.0 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mdit-py-plugins?source=hash-mapping - size: 42180 - timestamp: 1733854816517 + size: 15175 + timestamp: 1761214578417 - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda sha256: 123cc004e2946879708cdb6a9eff24acbbb054990d6131bb94bca7a374ebebfc md5: 1997a083ef0b4c9331f9191564be275e @@ -6148,53 +5525,56 @@ packages: - pkg:pypi/mdurl?source=hash-mapping size: 14465 timestamp: 1733255681319 -- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.3-pyh29332c3_0.conda - sha256: a67484d7dd11e815a81786580f18b6e4aa2392f292f29183631a6eccc8dc37b3 - md5: 7ec6576e328bc128f4982cd646eeba85 +- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + sha256: b52dc6c78fbbe7a3008535cb8bfd87d70d8053e9250bbe16e387470a9df07070 + md5: b97e84d1553b4a1c765b87fff83453ad depends: - - python >=3.9 + - python >=3.10 - typing_extensions - python license: BSD-3-Clause - license_family: BSD purls: - - pkg:pypi/mistune?source=hash-mapping - size: 72749 - timestamp: 1742402716323 -- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py312h66e93f0_0.conda - sha256: b57c8bd233087479c70cb3ee3420861e0625b8a5a697f5abe41f5103fb2c2e69 - md5: a84061bc7e166712deb33bf7b32f756d + - pkg:pypi/mistune?source=compressed-mapping + size: 74567 + timestamp: 1777824616382 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py312h4c3975b_0.conda + sha256: 2c03499b0f267a29321ce198a86285449eca2bb685e883703ef564a2ce641802 + md5: e3174d3f01d539ffd867a85639a8d9b5 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.12,<3.13.0a0 + - python-librt >=0.8.0 - python_abi 3.12.* *_cp312 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=compressed-mapping - size: 18664849 - timestamp: 1738767977895 -- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.15.0-py313h536fd9c_0.conda - sha256: ba62b6ccf6775290dcc4ca01c160b29f1fb67300928609fff60126fdae38034d - md5: 80b1cac6f9ca2ab7d96690b8aff3114d + size: 22035539 + timestamp: 1776802000447 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py313h07c4f96_0.conda + sha256: ae1b57248911cd9adbbf2ca1e8ebf81b0369482d5b4c628f619f15c3273ea18d + md5: 6c43e875d6f70a89909697178d2d08c4 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.13,<3.14.0a0 + - python-librt >=0.8.0 - python_abi 3.13.* *_cp313 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=hash-mapping - size: 17058016 - timestamp: 1738767732637 + - pkg:pypi/mypy?source=compressed-mapping + size: 21971694 + timestamp: 1776802010332 - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.20.2-py314h5bd0f2a_0.conda sha256: ff90f20225011cbf8e5ae9b97e1b445135b19e633b5d3b0320e02d763a324054 md5: 06b2a90aa4363ac50c959926bc384dea @@ -6214,38 +5594,42 @@ packages: - pkg:pypi/mypy?source=compressed-mapping size: 20297545 timestamp: 1776802022720 -- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py312h01d7ebd_0.conda - sha256: 38132c4b5de6686965f21b51a1656438e83b2a53d6f50e9589e73fb57a43dd49 - md5: 0251bb4d6702b729b06fd5c7918e9242 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py312h933eb07_0.conda + sha256: e72b842696f09b4dc794dbb501197af2e940144a1404f50076109e87eb0b18c8 + md5: 539fff6aab601858e1915d50e439d152 depends: - - __osx >=10.13 + - __osx >=11.0 - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.12,<3.13.0a0 + - python-librt >=0.8.0 - python_abi 3.12.* *_cp312 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 12384787 - timestamp: 1738768017667 -- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.15.0-py313h63b0ddb_0.conda - sha256: ec50dc7be70eff5008d73b4bd29fba72e02e499e9b60060a49ece4c1e12a9d55 - md5: e9dc60a2c2c62f4d2e24f61603f00bdc + size: 14710079 + timestamp: 1776802776234 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py313hf59fe81_0.conda + sha256: 543bb8999db36dae32d890cf9552fbe6090be5458acb34c977cfc05e853d83d6 + md5: 4605e0caa9402f6b36c1af93ab83197d depends: - - __osx >=10.13 + - __osx >=11.0 - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.13,<3.14.0a0 + - python-librt >=0.8.0 - python_abi 3.13.* *_cp313 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=hash-mapping - size: 11022410 - timestamp: 1738768159908 + - pkg:pypi/mypy?source=compressed-mapping + size: 14962980 + timestamp: 1776802953444 - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.20.2-py314h217eccc_0.conda sha256: 7a9e6f0b4c13622a3fba514472bee5130e9254fc2c8ef689b9ff9783f50492c8 md5: bcf7143c9f4b1bd763d715815fb98fd9 @@ -6264,40 +5648,44 @@ packages: - pkg:pypi/mypy?source=hash-mapping size: 13069509 timestamp: 1776803940390 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py312hea69d52_0.conda - sha256: 7284d77173d385f5c7456c13d825dbae170920a31ca7a0996d2608ad17f17e2f - md5: 909034322685579577b1bbb9b47e39e1 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py312hefc2c51_0.conda + sha256: 1e51597a605b2160bce7348cfa98ba50bb9d1f8a9c84c900ec439bde5c7da255 + md5: 7b0f9713b276fe257a55a4a0087bafa7 depends: - __osx >=11.0 - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython + - python-librt >=0.8.0 - python_abi 3.12.* *_cp312 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=hash-mapping - size: 10149670 - timestamp: 1738768707592 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.15.0-py313h90d716c_0.conda - sha256: 4dc7a5a30017c742c204311afd078c639ca434b7f44835dfba789a5fb972ea6c - md5: d01a9742c8e3c425d3c3d5e412a43872 + - pkg:pypi/mypy?source=compressed-mapping + size: 11889344 + timestamp: 1776803962764 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py313hd3e6d80_0.conda + sha256: b734fbd229cdfab3c1caa1abb4ce26059151c67ec5d918120d93d163b11a680b + md5: a3117e1e95fb48d0b5252f3fc43ab7b8 depends: - __osx >=11.0 - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.13,<3.14.0a0 - python >=3.13,<3.14.0a0 *_cp313 + - python-librt >=0.8.0 - python_abi 3.13.* *_cp313 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=compressed-mapping - size: 10275919 - timestamp: 1738768578918 + - pkg:pypi/mypy?source=hash-mapping + size: 12024544 + timestamp: 1776803045716 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.20.2-py314hbdd0d06_0.conda sha256: 4753e5a40d6cc0ba0fbf42b43b639cd4fcc2fadbeed6571589c6dc6b8c856671 md5: 19e51df1d38f05a2384771cdcbe355a7 @@ -6317,42 +5705,46 @@ packages: - pkg:pypi/mypy?source=compressed-mapping size: 12162223 timestamp: 1776802958871 -- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py312h4389bb4_0.conda - sha256: 3bab35d2f17f9b2c8498c952f7d182848f2d70775e7e970d5f53c7eeb87741a6 - md5: 1eea4f4c0038b6f9b399dfad2305cd6f +- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py312he06e257_0.conda + sha256: bf8bba1583c2d1a63e0381dd6a82cafba367eee269e413111a31a1722d57f719 + md5: e9d0596785a720e28e61de14d6dd40f0 depends: - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.12,<3.13.0a0 + - python-librt >=0.8.0 - python_abi 3.12.* *_cp312 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 9852020 - timestamp: 1738768035931 -- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.15.0-py313ha7868ed_0.conda - sha256: b84e3e51b6a98d5cff5e036c2366eb453a4e592891ec6ff3ab850ae27ba84322 - md5: efa5e67ca0b6d09cc2e149bee2001073 + size: 11734892 + timestamp: 1776802455015 +- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py313h5ea7bf4_0.conda + sha256: f0e03ff8c515a5b816231e87060972bd832aab93481ba16030cea41da146f616 + md5: d71813f859bf8b589ae9bda43f228ea3 depends: - mypy_extensions >=1.0.0 + - pathspec >=1.0.0 - psutil >=4.0 - python >=3.13,<3.14.0a0 + - python-librt >=0.8.0 - python_abi 3.13.* *_cp313 - - typing_extensions >=4.1.0 + - typing_extensions >=4.6.0 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: - pkg:pypi/mypy?source=hash-mapping - size: 8300827 - timestamp: 1738768501453 + size: 11700290 + timestamp: 1776802394079 - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.20.2-py314h5a2d7ad_0.conda sha256: 1480cdef0b28c52494432df2deb2c379d5892e04695ca823217c79860cd71bdf md5: c9dffe30976574155a3c77eafc3ba15f @@ -6373,17 +5765,6 @@ packages: - pkg:pypi/mypy?source=compressed-mapping size: 9760683 timestamp: 1776802300096 -- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - sha256: 1895f47b7d68581a6facde5cb13ab8c2764c2e53a76bd746f8f98910dc4e08fe - md5: 29097e7ea634a45cc5386b95cac6568f - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mypy-extensions?source=hash-mapping - size: 10854 - timestamp: 1733230986902 - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 md5: e9c622e0d00fa24a6292279af3ab6d06 @@ -6392,26 +5773,9 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/mypy-extensions?source=hash-mapping - size: 11766 - timestamp: 1745776666688 -- conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - sha256: f035d0ea623f63247f0f944eb080eaa2a45fb5b7fda8947f4ac94d381ef3bf33 - md5: b528795158847039003033ee0db20e9b - depends: - - docutils >=0.19,<0.22 - - jinja2 - - markdown-it-py >=3.0.0,<4.0.0 - - mdit-py-plugins >=0.4.1,<1 - - python >=3.10 - - pyyaml - - sphinx >=7,<9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/myst-parser?source=hash-mapping - size: 73074 - timestamp: 1739381945342 + - pkg:pypi/mypy-extensions?source=hash-mapping + size: 11766 + timestamp: 1745776666688 - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.0.0-pyhd8ed1ab_0.conda sha256: f352d594d968acd31052c5f894ae70718be56481ffa9c304fdfcbe78ddf66eb1 md5: a65e2c3c764766f0b28a3ac5052502a6 @@ -6429,9 +5793,9 @@ packages: - pkg:pypi/myst-parser?source=hash-mapping size: 73535 timestamp: 1768942892170 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - sha256: a20cff739d66c2f89f413e4ba4c6f6b59c50d5c30b5f0d840c13e8c9c2df9135 - md5: 6bb0d77277061742744176ab555b723c +- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + sha256: 1b66960ee06874ddceeebe375d5f17fb5f393d025a09e15b830ad0c4fffb585b + md5: 00f5b8dafa842e0c27c1cd7296aa4875 depends: - jupyter_client >=6.1.12 - jupyter_core >=4.12,!=5.0.* @@ -6442,11 +5806,11 @@ packages: license_family: BSD purls: - pkg:pypi/nbclient?source=hash-mapping - size: 28045 - timestamp: 1734628936013 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - sha256: dcccb07c5a1acb7dc8be94330e62d54754c0e9c9cb2bb6865c8e3cfe44cf5a58 - md5: d24beda1d30748afcc87c429454ece1b + size: 28473 + timestamp: 1766485646962 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda + sha256: ab2ac79c5892c5434d50b3542d96645bdaa06d025b6e03734be29200de248ac2 + md5: 2bce0d047658a91b99441390b9b27045 depends: - beautifulsoup4 - bleach-with-css !=5.0.0 @@ -6462,18 +5826,18 @@ packages: - packaging - pandocfilters >=1.4.1 - pygments >=2.4.1 - - python >=3.9 + - python >=3.10 - traitlets >=5.1 - python constrains: - pandoc >=2.9.2,<4.0.0 - - nbconvert ==7.16.6 *_0 + - nbconvert ==7.17.1 *_0 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/nbconvert?source=hash-mapping - size: 200601 - timestamp: 1738067871724 + - pkg:pypi/nbconvert?source=compressed-mapping + size: 202229 + timestamp: 1775615493260 - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 md5: bbe1963f1e47f594070ffe87cdf612ea @@ -6489,16 +5853,6 @@ packages: - pkg:pypi/nbformat?source=hash-mapping size: 100945 timestamp: 1733402844974 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 - md5: 47e340acb35de30501a76c7c799c41d7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: X11 AND BSD-3-Clause - purls: [] - size: 891641 - timestamp: 1738195959188 - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 md5: fc21868a1a5aacc937e7a18747acb8a5 @@ -6509,15 +5863,6 @@ packages: purls: [] size: 918956 timestamp: 1777422145199 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - sha256: ea4a5d27ded18443749aefa49dc79f6356da8506d508b5296f60b8d51e0c4bd9 - md5: ced34dd9929f491ca6dab6a2927aff25 - depends: - - __osx >=10.13 - license: X11 AND BSD-3-Clause - purls: [] - size: 822259 - timestamp: 1738196181298 - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda sha256: f5f7e006ff4271305ab4cc08eedd855c67a571793c3d18aff73f645f088a8cae md5: 31b8740cf1b2588d4e61c81191004061 @@ -6527,15 +5872,6 @@ packages: purls: [] size: 831711 timestamp: 1777423052277 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 - md5: 068d497125e4bf8a66bf707254fff5ae - depends: - - __osx >=11.0 - license: X11 AND BSD-3-Clause - purls: [] - size: 797030 - timestamp: 1738196177597 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d md5: 343d10ed5b44030a2f67193905aea159 @@ -6568,18 +5904,6 @@ packages: - pkg:pypi/notebook-shim?source=hash-mapping size: 16817 timestamp: 1733408419340 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda - sha256: cbf62df3c79a5c2d113247ddea5658e9ff3697b6e741c210656e239ecaf1768f - md5: 41adf927e746dc75ecf0ef841c454e48 - depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=13 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 2939306 - timestamp: 1739301879343 - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb md5: da1b85b6a87e141f5140bb9924cecab0 @@ -6592,17 +5916,6 @@ packages: purls: [] size: 3167099 timestamp: 1775587756857 -- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.1-hc426f3f_0.conda - sha256: 505a46671dab5d66df8e684f99a9ae735a607816b12810b572d63caa512224df - md5: a7d63f8e7ab23f71327ea6d27e2d5eae - depends: - - __osx >=10.13 - - ca-certificates - license: Apache-2.0 - license_family: Apache - purls: [] - size: 2591479 - timestamp: 1739302628009 - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.2-hc881268_0.conda sha256: 334fd49ea31b99114f5afb1ec44555dc8c90640648302a4f8f838ee345d1ec50 md5: 5cf0ece4375c73d7a5765e83565a69c7 @@ -6614,17 +5927,6 @@ packages: purls: [] size: 2776564 timestamp: 1775589970694 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.1-h81ee809_0.conda - sha256: 4f8e2389e1b711b44182a075516d02c80fa7a3a7e25a71ff1b5ace9eae57a17a - md5: 75f9f0c7b1740017e2db83a53ab9a28e - depends: - - __osx >=11.0 - - ca-certificates - license: Apache-2.0 - license_family: Apache - purls: [] - size: 2934522 - timestamp: 1739301896733 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea md5: 25dcccd4f80f1638428613e0d7c9b4e1 @@ -6636,19 +5938,6 @@ packages: purls: [] size: 3106008 timestamp: 1775587972483 -- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda - sha256: 56dcc2b4430bfc1724e32661c34b71ae33a23a14149866fc5645361cfd3b3a6a - md5: 0730f8094f7088592594f9bf3ae62b3f - depends: - - ca-certificates - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 8515197 - timestamp: 1739304103653 - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 md5: 05c7d624cff49dbd8db1ad5ba537a8a3 @@ -6674,17 +5963,6 @@ packages: - pkg:pypi/overrides?source=hash-mapping size: 30139 timestamp: 1734587755455 -- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1 - md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa - depends: - - python >=3.8 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/packaging?source=hash-mapping - size: 60164 - timestamp: 1733203368787 - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 md5: 4c06a92e74452cfa53623a81592e8934 @@ -6708,28 +5986,18 @@ packages: - pkg:pypi/pandocfilters?source=hash-mapping size: 11627 timestamp: 1631603397334 -- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - sha256: 17131120c10401a99205fc6fe436e7903c0fa092f1b3e80452927ab377239bcc - md5: 5c092057b6badd30f75b06244ecd01c9 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + sha256: 611882f7944b467281c46644ffde6c5145d1a7730388bcde26e7e86819b0998e + md5: 39894c952938276405a1bd30e4ce2caf depends: - - python >=3.9 + - python >=3.10 + - python license: MIT license_family: MIT purls: - - pkg:pypi/parso?source=hash-mapping - size: 75295 - timestamp: 1733271352153 -- conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - sha256: 9f64009cdf5b8e529995f18e03665b03f5d07c0b17445b8badef45bde76249ee - md5: 617f15191456cc6a13db418a275435e5 - depends: - - python >=3.9 - license: MPL-2.0 - license_family: MOZILLA - purls: - - pkg:pypi/pathspec?source=hash-mapping - size: 41075 - timestamp: 1733233471940 + - pkg:pypi/parso?source=compressed-mapping + size: 82472 + timestamp: 1777722955579 - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda sha256: 6eaee417d33f298db79bc7185ab1208604c0e6cf51dade34cd513c6f9db9c6f3 md5: 11adc78451c998c0fd162584abfa3559 @@ -6752,17 +6020,6 @@ packages: - pkg:pypi/pexpect?source=hash-mapping size: 53561 timestamp: 1733302019362 -- conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - sha256: e2ac3d66c367dada209fc6da43e645672364b9fd5f9d28b9f016e24b81af475b - md5: 11a9d1d09a3615fc07c3faf79bc0b943 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pickleshare?source=hash-mapping - size: 11748 - timestamp: 1733327448200 - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda sha256: 353fd5a2c3ce31811a6272cd328874eb0d327b1eafd32a1e19001c4ad137ad3a md5: dc702b2fae7ebe770aff3c83adb16b63 @@ -6774,39 +6031,18 @@ packages: - pkg:pypi/pkginfo?source=hash-mapping size: 30536 timestamp: 1739984682585 -- conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - sha256: adb2dde5b4f7da70ae81309cce6188ed3286ff280355cf1931b45d91164d2ad8 - md5: 5a5870a74432aa332f7d32180633ad05 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda + sha256: 8f29915c172f1f7f4f7c9391cd5dac3ebf5d13745c8b7c8006032615246345a5 + md5: 89c0b6d1793601a2a3a3f7d2d3d8b937 depends: - - python >=3.9 - license: MIT AND PSF-2.0 - purls: - - pkg:pypi/pkgutil-resolve-name?source=hash-mapping - size: 10693 - timestamp: 1733344619659 -- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda - sha256: 0f48999a28019c329cd3f6fd2f01f09fc32cc832f7d6bbe38087ddac858feaa3 - md5: 424844562f5d337077b445ec6b1398a7 - depends: - - python >=3.9 + - python >=3.10 - python license: MIT license_family: MIT purls: - - pkg:pypi/platformdirs?source=compressed-mapping - size: 23531 - timestamp: 1746710438805 -- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - sha256: 122433fc5318816b8c69283aaf267c73d87aa2d09ce39f64c9805c9a3b264819 - md5: e9dcbce5f45f9ee500e728ae58b605b6 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pluggy?source=hash-mapping - size: 23595 - timestamp: 1733222855563 + - pkg:pypi/platformdirs?source=hash-mapping + size: 25862 + timestamp: 1775741140609 - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e md5: d7585b6550ad04c8c5e21097ada2888e @@ -6819,59 +6055,59 @@ packages: - pkg:pypi/pluggy?source=hash-mapping size: 25877 timestamp: 1764896838868 -- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.22.1-pyhd8ed1ab_0.conda - sha256: 454e2c0ef14accc888dd2cd2e8adb8c6a3a607d2d3c2f93962698b5718e6176d - md5: c64b77ccab10b822722904d889fa83b5 +- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c + md5: a11ab1f31af799dd93c3a39881528884 depends: - - python >=3.9 + - python >=3.10 license: Apache-2.0 license_family: Apache purls: - - pkg:pypi/prometheus-client?source=hash-mapping - size: 52641 - timestamp: 1748896836631 -- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda - sha256: ebc1bb62ac612af6d40667da266ff723662394c0ca78935340a5b5c14831227b - md5: d17ae9db4dc594267181bd199bf9a551 + - pkg:pypi/prometheus-client?source=compressed-mapping + size: 57113 + timestamp: 1775771465170 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae + md5: edb16f14d920fb3faf17f5ce582942d6 depends: - - python >=3.9 + - python >=3.10 - wcwidth constrains: - - prompt_toolkit 3.0.51 + - prompt_toolkit 3.0.52 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/prompt-toolkit?source=compressed-mapping - size: 271841 - timestamp: 1744724188108 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda - sha256: 158047d7a80e588c846437566d0df64cec5b0284c7184ceb4f3c540271406888 - md5: 8e30db4239508a538e4a3b3cdf5b9616 + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 273927 + timestamp: 1756321848365 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda + sha256: d834fd656133c9e4eaf63ffe9a117c7d0917d86d89f7d64073f4e3a0020bd8a7 + md5: dd94c506b119130aef5a9382aed648e7 depends: + - python + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/psutil?source=hash-mapping - size: 466219 - timestamp: 1740663246825 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py313h536fd9c_0.conda - sha256: 1b39f0ce5a345779d70c885664d77b5f8ef49f7378829bd7286a7fb98b7ea852 - md5: 8f315d1fce04a046c1b93fa6e536661d + - pkg:pypi/psutil?source=compressed-mapping + size: 225545 + timestamp: 1769678155334 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + sha256: f19fd682d874689dfde20bf46d7ec1a28084af34583e0405685981363af47c91 + md5: 25fe6e02c2083497b3239e21b49d8093 depends: + - python - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13,<3.14.0a0 + - libgcc >=14 - python_abi 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 475101 - timestamp: 1740663284505 + size: 228663 + timestamp: 1769678153829 - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py314h0f05182_0.conda sha256: f15574ed6c8c8ed8c15a0c5a00102b1efe8b867c0bd286b498cd98d95bd69ae5 md5: 4f225a966cfee267a79c5cb6382bd121 @@ -6886,32 +6122,32 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 231303 timestamp: 1769678156552 -- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py312h01d7ebd_0.conda - sha256: bdfa40a1ef3a80c3bec425a5ed507ebda2bdebce2a19bccb000db9d5c931750c - md5: fcad6b89f4f7faa999fa4d887eab14ba +- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda + sha256: 517c17b24349476535db4da7d1cd31538dadf2c77f9f7f7d8be6b7dc5dfbb636 + md5: 1fd947fae149960538fc941b8f122bc1 depends: + - python - __osx >=10.13 - - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 473946 - timestamp: 1740663466925 -- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.0.0-py313h63b0ddb_0.conda - sha256: b117f61eaf3d5fb640d773c3021f222c833a69c2ac123d7f4b028b3e5d638dd4 - md5: 2c8969aaee2cf24bc8931f5fc36cccfd + size: 236338 + timestamp: 1769678402626 +- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py313h16366db_0.conda + sha256: b50a9d64aabd30c05e405cc1166f21fd7dee8d1b42ef38116701883d3bd4d5fa + md5: c8185e1891ace76e565b4c28dd50ed5d depends: + - python - __osx >=10.13 - - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 482494 - timestamp: 1740663492867 + size: 239894 + timestamp: 1769678319684 - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py314hd330473_0.conda sha256: 3194ce0d94c810cb1809da851261be34e1cae72ca345445b29e61766b38ee6cc md5: d465805e603072c341554159939be5b8 @@ -6925,34 +6161,34 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 242816 timestamp: 1769678225798 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda - sha256: cb11dcb39b2035ef42c3df89b5a288744b5dcb5a98fb47385760843b1d4df046 - md5: 0f461bd37cb428dc20213a08766bb25d +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda + sha256: 6d0e21c76436374635c074208cfeee62a94d3c37d0527ad67fd8a7615e546a05 + md5: fd856899666759403b3c16dcba2f56ff depends: + - python - __osx >=11.0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython + - python 3.12.* *_cpython - python_abi 3.12.* *_cp312 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 476376 - timestamp: 1740663381256 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py313h90d716c_0.conda - sha256: a3d8376cf24ee336f63d3e6639485b68c592cf5ed3e1501ac430081be055acf9 - md5: 21105780750e89c761d1c72dc5304930 + size: 239031 + timestamp: 1769678393511 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py313h6688731_0.conda + sha256: 1d2a6039fb71d61134b1d6816202529f2f6286c83b59bc1491fd288f5c08046e + md5: ba2d89e51a855963c767648f44c03871 depends: + - python - __osx >=11.0 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 + - python 3.13.* *_cp313 - python_abi 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 484139 - timestamp: 1740663381126 + size: 242596 + timestamp: 1769678288893 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py314ha14b1ff_0.conda sha256: e0f31c053eb11803d63860c213b2b1b57db36734f5f84a3833606f7c91fedff9 md5: fc4c7ab223873eee32080d51600ce7e7 @@ -6967,36 +6203,36 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 245502 timestamp: 1769678303655 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda - sha256: 088451ee2c9a349e1168f70afe275e58f86350faffb09c032cff76f97d4fb7bb - md5: f5b86d6e2e645ee276febe79a310b640 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda + sha256: edffc84c001a05b996b5f8607c8164432754e86ec9224e831cd00ebabdec04e7 + md5: a2724c93b745fc7861948eb8b9f6679a depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python_abi 3.12.* *_cp312 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 484682 - timestamp: 1740663813103 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py313ha7868ed_0.conda - sha256: d8e5d86e939d5f308c7922835a94458afb29d81c90b5d43c43a5537c9c7adbc1 - md5: 3cdf99cf98b01856af9f26c5d8036353 + size: 242769 + timestamp: 1769678170631 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py313h5fd188c_0.conda + sha256: 3ec3373748f83069bef93b540de416e637ee30231b222d5df8f712e93f2f9195 + md5: 761b299a6289c77459defea3563f8fc0 depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python_abi 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 491314 - timestamp: 1740663777370 + size: 246062 + timestamp: 1769678176886 - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py314hc5dbbe4_0.conda sha256: 17c8274ce5a32c9793f73a5a0094bd6188f3a13026a93147655143d4df034214 md5: fd539ac231820f64066839251aa9fa48 @@ -7045,24 +6281,6 @@ packages: - pkg:pypi/pycparser?source=hash-mapping size: 110100 timestamp: 1733195786147 -- conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - sha256: 073473ba9c0cc3946026dde9112d2edb0ac52f6cc35d2126f4bff8bad1cc74a6 - md5: 837aaf71ddf3b27acae0e7e9015eebc6 - depends: - - accessible-pygments - - babel - - beautifulsoup4 - - docutils !=0.17.0 - - pygments >=2.7 - - python >=3.9 - - sphinx >=6.1 - - typing_extensions - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pydata-sphinx-theme?source=hash-mapping - size: 1547597 - timestamp: 1734446468767 - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.17.1-pyhcf101f3_0.conda sha256: 71161705133df512054177ad03f394e073c39e369dda52fda8e8e0a5371df8c2 md5: 620cee61c85cf6a407f80e8d502796ec @@ -7082,17 +6300,6 @@ packages: - pkg:pypi/pydata-sphinx-theme?source=hash-mapping size: 1657335 timestamp: 1776777605561 -- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - sha256: 28a3e3161390a9d23bc02b4419448f8d27679d9e2c250e29849e37749c8de86b - md5: 232fb4577b6687b2d503ef8e254270c9 - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/pygments?source=hash-mapping - size: 888600 - timestamp: 1736243563082 - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 md5: 16c18772b340887160c79a6acc022db0 @@ -7104,12 +6311,12 @@ packages: - pkg:pypi/pygments?source=compressed-mapping size: 893031 timestamp: 1774796815820 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-11.0-py312h2365019_0.conda - sha256: 91a27ede294fec129d115f2e0b0ce881f0c12332ee5e9c33ba522c037ad14bbb - md5: 0925c0e6ee32098c461423ea93490b97 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-12.1-py312h4a480f0_0.conda + sha256: ecf778f886aaf50db22c0971fb0873f0dbe25663f124bd714bc87b4d0925f534 + md5: 18a20cb8c3e19f0b3799a48eba5b44aa depends: - __osx >=10.13 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - setuptools @@ -7117,14 +6324,29 @@ packages: license_family: MIT purls: - pkg:pypi/pyobjc-core?source=hash-mapping - size: 489634 - timestamp: 1736891165910 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.0-py312hb9d441b_0.conda - sha256: 7805d910dd6ac686e2f780c879a986f35d7a4c73f4236c956c03bdcb26bec421 - md5: 0726db04477a28c51d1a260afb356b67 + size: 487397 + timestamp: 1763151480498 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-core-12.1-py313h07bcf3a_0.conda + sha256: 1e0edd34b804e20ba064dcebcfce3066d841ec812f29ed65902da7192af617d1 + md5: 6a2c3a617a70f97ca53b7b88461b1c27 + depends: + - __osx >=10.13 + - libffi >=3.5.2,<3.6.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - setuptools + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyobjc-core?source=hash-mapping + size: 491157 + timestamp: 1763151415674 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py312h19bbe71_0.conda + sha256: b015f430fe9ea2c53e14be13639f1b781f68deaa5ae74cd8c1d07720890cd02a + md5: c65d7abdc9e60fd3af0ed852591adf1b depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 @@ -7133,46 +6355,61 @@ packages: license_family: MIT purls: - pkg:pypi/pyobjc-core?source=hash-mapping - size: 478921 - timestamp: 1736891272846 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.0-py313hb6afeec_0.conda - sha256: 60dcbfdd022902f3492a38af181eb17310f93d9b87ca2bf70794fd58ac38a45a - md5: 6a46199aebac189cc979358d52e098f4 + size: 476750 + timestamp: 1763151865523 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-12.1-py314h3a4d195_0.conda + sha256: df5af268c5a74b7160d772c263ece6f43257faff571783443e34b5f1d5a61cf2 + md5: 75a84fc8337557347252cc4fd3ba2a93 depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - libffi >=3.5.2,<3.6.0a0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 - setuptools license: MIT license_family: MIT purls: - pkg:pypi/pyobjc-core?source=hash-mapping - size: 482419 - timestamp: 1736891038169 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-11.0-py312h2365019_0.conda - sha256: 974fc6659f162a6e9cf201e5544f32d5c38d795a1141b327f87be2821dc7bf07 - md5: 2486dd4f176f772531e0ecf22a8b85bd + size: 483374 + timestamp: 1763151489724 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-12.1-py312h1993040_0.conda + sha256: 3a29ca3cc2044b408447ff86ae0c57ecc3ff805a8fc838525610921024c8521a + md5: b6881a919e1bfd66349e2260b163dc7c depends: - __osx >=10.13 - - libffi >=3.4,<4.0a0 - - pyobjc-core 11.0.* + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping - size: 381786 - timestamp: 1736927108218 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.0-py312hb9d441b_0.conda - sha256: 53d099865f8f758029708f4365ee7c9184d9ffcc8fc8210971b723a3936f9c00 - md5: dc263e6e18b32318a43252dbb0596ad4 + size: 375580 + timestamp: 1763160526695 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyobjc-framework-cocoa-12.1-py313hf669bc3_0.conda + sha256: 4761b8448bb9ecfcd9636a506104e6474e0f4cb73d108f2088997702ae984a00 + md5: 628b5ad83d6140fe4bfa937e2f357ed7 + depends: + - __osx >=10.13 + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping + size: 374120 + timestamp: 1763160397755 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py312h1de3e18_0.conda + sha256: 3710f5ae09c2ea77ba4d82cc51e876d9fc009b878b197a40d3c6347c09ae7d7c + md5: f0bae1b67ece138378923e340b940051 depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 - - pyobjc-core 11.0.* + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* - python >=3.12,<3.13.0a0 - python >=3.12,<3.13.0a0 *_cpython - python_abi 3.12.* *_cp312 @@ -7180,24 +6417,24 @@ packages: license_family: MIT purls: - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping - size: 383608 - timestamp: 1736927118445 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.0-py313hb6afeec_0.conda - sha256: ae1f04dfe889a52628e4886b8fa6f1e8696b2b9bc5dd074e4d11c001e49ba249 - md5: 63722167812348a37aae8f062ad88590 + size: 377723 + timestamp: 1763160705325 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-12.1-py314h36abed7_0.conda + sha256: aa76ee4328d0514d7c1c455dcd2d3b547db1c59797e54ce0a3f27de5b970e508 + md5: 4219bb3408016e22316cf8b443b5ef93 depends: - __osx >=11.0 - - libffi >=3.4,<4.0a0 - - pyobjc-core 11.0.* - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - libffi >=3.5.2,<3.6.0a0 + - pyobjc-core 12.1.* + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT purls: - pkg:pypi/pyobjc-framework-cocoa?source=hash-mapping - size: 384331 - timestamp: 1736927195004 + size: 374792 + timestamp: 1763160601898 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.21.1-py310hd8a072f_0.conda noarch: python sha256: b2c870bbb3b8390ee2ff286f36e9cf8c2a112996874c64832929a9781c336db7 @@ -7217,25 +6454,6 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 4238093 timestamp: 1776177970104 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyproject-fmt-2.5.1-py39h77e2912_1.conda - noarch: python - sha256: e54f69d8635daa04c0dbb868bc8eece8a8db61d231babd8887051227dc63651c - md5: fd4d1d120869896c3a2f0e60881c6554 - depends: - - python - - toml-fmt-common ==1.0.1 - - libgcc >=13 - - __glibc >=2.17,<3.0.a0 - - _python_abi3_support 1.* - - python >=3.9 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyproject-fmt?source=hash-mapping - size: 1603752 - timestamp: 1740150937392 - conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.21.1-py310hb9b2626_0.conda noarch: python sha256: 6aa524f2fe6d86011b0100a1dd12dbdb311c725bd0f8ef5248868b5f41fb308a @@ -7254,24 +6472,6 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 4127911 timestamp: 1776178140236 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyproject-fmt-2.5.1-py39h286ba15_1.conda - noarch: python - sha256: 1f6687a6aa09ff76a7a47cc95f9a2da43ef58f50ecfeafd32962edd0391d4fd6 - md5: 4412404088615123054395dfc55716cb - depends: - - python - - toml-fmt-common ==1.0.1 - - __osx >=10.13 - - _python_abi3_support 1.* - - python >=3.9 - constrains: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyproject-fmt?source=hash-mapping - size: 1504931 - timestamp: 1740150987346 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.21.1-py310h3b8a9b8_0.conda noarch: python sha256: 22cb944b345d38e5ce18c95b9b41f6a816c09d172af1b0586e1f8b1de431c6a0 @@ -7290,40 +6490,6 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 3879847 timestamp: 1776178129040 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py312h6f6235b_0.conda - sha256: 7f3e81bf127c894140bf608f71e3f96d902eeaf2525b766b0f99a04f6371084e - md5: f64845e77fa7c6262abd68e190989a7f - depends: - - __osx >=11.0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - - toml-fmt-common 1.0.1 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyproject-fmt?source=hash-mapping - size: 1184712 - timestamp: 1730382427331 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproject-fmt-2.5.0-py313hf8519d8_0.conda - sha256: bff9d3d9bcd706e8fcc0117092a9a8cbf39c1186a6c9f02c8a44bdc7d5f583dc - md5: 0ab55657cf9b924764282288f177a4c6 - depends: - - __osx >=11.0 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 - - toml-fmt-common 1.0.1 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyproject-fmt?source=hash-mapping - size: 1184361 - timestamp: 1730382555877 - conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.21.1-py310ha413424_0.conda noarch: python sha256: c7056dcd5cb72512299a997861f01b9d2e54edceac9d4d88e9f1035d764754ea @@ -7342,27 +6508,6 @@ packages: - pkg:pypi/pyproject-fmt?source=hash-mapping size: 4260906 timestamp: 1776178177936 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyproject-fmt-2.5.1-py39he870945_1.conda - noarch: python - sha256: 4cc2b5024b7dac334eedab339e11bf78180dcad71e31f3090982645f9d4d6bfc - md5: a402c3ab89aa14cbceb516d3af52c5e1 - depends: - - python - - toml-fmt-common ==1.0.1 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - _python_abi3_support 1.* - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyproject-fmt?source=hash-mapping - size: 1338496 - timestamp: 1740150971819 - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca md5: e2fd202833c4a981ce8a65974fe4abd1 @@ -7388,25 +6533,6 @@ packages: - pkg:pypi/pysocks?source=hash-mapping size: 21085 timestamp: 1733217331982 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - sha256: 963524de7340c56615583ba7b97a6beb20d5c56a59defb59724dc2a3105169c9 - md5: c3c9316209dec74a705a36797970c6be - depends: - - colorama - - exceptiongroup >=1.0.0rc8 - - iniconfig - - packaging - - pluggy <2,>=1.5 - - python >=3.9 - - tomli >=1 - constrains: - - pytest-faulthandler >=2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pytest?source=hash-mapping - size: 259816 - timestamp: 1740946648058 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.3-pyhc364b38_1.conda sha256: 960f59442173eee0731906a9077bd5ccf60f4b4226f05a22d1728ab9a21a879c md5: 6a991452eadf2771952f39d43615bb3e @@ -7428,20 +6554,6 @@ packages: - pkg:pypi/pytest?source=compressed-mapping size: 299984 timestamp: 1775644472530 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - sha256: 09acac1974e10a639415be4be326dd21fa6d66ca51a01fb71532263fba6dccf6 - md5: 79963c319d1be62c8fd3e34555816e01 - depends: - - coverage >=7.5 - - pytest >=4.6 - - python >=3.9 - - toml - license: MIT - license_family: MIT - purls: - - pkg:pypi/pytest-cov?source=hash-mapping - size: 26256 - timestamp: 1733223113491 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda sha256: 44e42919397bd00bfaa47358a6ca93d4c21493a8c18600176212ec21a8d25ca5 md5: 67d1790eefa81ed305b89d8e314c7923 @@ -7456,21 +6568,7 @@ packages: purls: - pkg:pypi/pytest-cov?source=compressed-mapping size: 29559 - timestamp: 1774139250481 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.1.1-pyhd8ed1ab_1.conda - sha256: a6af87cdb4cd981b33707147fc0ed37a5e4ea8322283a014947bccdfeff57a99 - md5: 010e50e74c467db278f1398a74106a04 - depends: - - jinja2 >=3.0.0 - - pytest >=7.0.0 - - pytest-metadata >=2.0.0 - - python >=3.9 - license: MPL-2.0 - license_family: MOZILLA - purls: - - pkg:pypi/pytest-html?source=hash-mapping - size: 25315 - timestamp: 1734739529167 + timestamp: 1774139250481 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-html-4.2.0-pyhd8ed1ab_0.conda sha256: 1b5441efb96fa7bf0a442b32b76c7adc37702b1589dfff10b80beb665e1ff76a md5: 12eaff6b42a938530c0c49a310260b2d @@ -7510,21 +6608,6 @@ packages: - pkg:pypi/pytest-metadata?source=hash-mapping size: 14532 timestamp: 1734146281190 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_1.conda - sha256: fb35da93084d653b86918c200abb2f0b88aceb3b0526c6aaa21b844f565ae237 - md5: 59aad4fb37cabc0bacc73cf344612ddd - depends: - - execnet >=2.1 - - pytest >=7.0.0 - - python >=3.9 - constrains: - - psutil >=3.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pytest-xdist?source=hash-mapping - size: 38147 - timestamp: 1733240891538 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda sha256: b7b58a5be090883198411337b99afb6404127809c3d1c9f96e99b59f36177a96 md5: 8375cfbda7c57fbceeda18229be10417 @@ -7540,60 +6623,59 @@ packages: - pkg:pypi/pytest-xdist?source=hash-mapping size: 39300 timestamp: 1751452761594 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda - build_number: 1 - sha256: 77f2073889d4c91a57bc0da73a0466d9164dbcf6191ea9c3a7be6872f784d625 - md5: d82342192dfc9145185190e651065aa9 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda + sha256: a44655c1c3e1d43ed8704890a91e12afd68130414ea2c0872e154e5633a13d7e + md5: 7eccb41177e15cc672e1babe9056018e depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 - - liblzma >=5.6.4,<6.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.49.1,<4.0a0 - - libuuid >=2.38.1,<3.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 - libxcrypt >=4.4.36 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 - - readline >=8.2,<9.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata constrains: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] - size: 31670716 - timestamp: 1741130026152 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda - build_number: 101 - sha256: cc1984ee54261cee6a2db75c65fc7d2967bc8c6e912d332614df15244d7730ef - md5: a7902a3611fe773da3921cbbf7bc2c5c + size: 31608571 + timestamp: 1772730708989 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.13-h6add32d_100_cp313.conda + build_number: 100 + sha256: 7f77eb57648f545c1f58e10035d0d9d66b0a0efb7c4b58d3ed89ec7269afdde1 + md5: 05051be49267378d2fcd12931e319ac3 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 - - liblzma >=5.6.4,<6.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.48.0,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libzlib >=1.3.1,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libuuid >=2.42,<3.0a0 + - libzlib >=1.3.2,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 + - openssl >=3.5.6,<4.0a0 - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 purls: [] - size: 33233150 - timestamp: 1739803603242 + size: 37358322 + timestamp: 1775614712638 python_site_packages_path: lib/python3.13/site-packages - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.4-habeac84_100_cp314.conda build_number: 100 @@ -7623,52 +6705,51 @@ packages: size: 36705460 timestamp: 1775614357822 python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.9-h9ccd52b_1_cpython.conda - build_number: 1 - sha256: c394f7068a714cad7853992f18292bb34c6d99fe7c21025664b05069c86b9450 - md5: b878567b6b749f993dbdbc2834115bc3 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.13-ha9537fe_0_cpython.conda + sha256: fb592ceb1bc247d19247d5535083da4a79721553e29e1290f5d81c07d4f086b5 + md5: ec05996c0d914a4e98ee3c7d789083f8 depends: - - __osx >=10.13 + - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 - - libsqlite >=3.49.1,<4.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 - - readline >=8.2,<9.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata constrains: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] - size: 13833024 - timestamp: 1741129416409 -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.2-h534c281_101_cp313.conda - build_number: 101 - sha256: 19abb6ba8a1af6985934a48f05fccd29ecc54926febdb8b3803f30134c518b34 - md5: 2e883c630979a183e23a510d470194e2 + size: 13672169 + timestamp: 1772730464626 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.13.13-h3d5d122_100_cp313.conda + build_number: 100 + sha256: 6f71b48fe93ebc0dd42c80358b75020f6ad12ed4772fb3555da36000139c0dc7 + md5: 8948c8c7c653ad668d55bbbd6836178b depends: - - __osx >=10.13 + - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.48.0,<4.0a0 - - libzlib >=1.3.1,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 + - openssl >=3.5.6,<4.0a0 - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 purls: [] - size: 13961675 - timestamp: 1739802065430 + size: 17650454 + timestamp: 1775616128232 python_site_packages_path: lib/python3.13/site-packages - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.4-h7c6738f_100_cp314.conda build_number: 100 @@ -7695,52 +6776,51 @@ packages: size: 14431104 timestamp: 1775616356805 python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.9-hc22306f_1_cpython.conda - build_number: 1 - sha256: fe804fc462396baab8abe525a722d0254c839533c98c47abd2c6d1248ad45e93 - md5: d9fac7b334ff6e5f93abd27509a53060 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.13-h8561d8f_0_cpython.conda + sha256: e658e647a4a15981573d6018928dec2c448b10c77c557c29872043ff23c0eb6a + md5: 8e7608172fa4d1b90de9a745c2fd2b81 depends: - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 - - libsqlite >=3.49.1,<4.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 - - readline >=8.2,<9.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata constrains: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] - size: 13042031 - timestamp: 1741128584924 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.2-h81fe080_101_cp313.conda - build_number: 101 - sha256: 6239a14c39a9902d6b617d57efe3eefbab23cf30cdc67122fdab81d04da193cd - md5: 71a76067a1cac1a2f03b43a08646a63e + size: 12127424 + timestamp: 1772730755512 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.13-h20e6be0_100_cp313.conda + build_number: 100 + sha256: d0fffc5fde21d1ae350da545dfb9e115a8c53bed8a9c5761f9efd4a5581853c1 + md5: 9991a930e81d3873eba7a299ba783ec4 depends: - __osx >=11.0 - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.48.0,<4.0a0 - - libzlib >=1.3.1,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.1,<4.0a0 + - openssl >=3.5.6,<4.0a0 - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata license: Python-2.0 purls: [] - size: 11682568 - timestamp: 1739801342527 + size: 12966447 + timestamp: 1775615694085 python_site_packages_path: lib/python3.13/site-packages - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.4-h4c637c5_100_cp314.conda build_number: 100 @@ -7767,52 +6847,51 @@ packages: size: 13533346 timestamp: 1775616188373 python_site_packages_path: lib/python3.14/site-packages -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.9-h3f84c4b_1_cpython.conda - build_number: 1 - sha256: 320acd0095442a451c4e0f0f896bed2f52b3b8f05df41774e5b0b433d9fa08e0 - md5: f0a0ad168b815fee4ce9718d4e6c1925 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda + sha256: a02b446d8b7b167b61733a3de3be5de1342250403e72a63b18dac89e99e6180e + md5: 2956dff38eb9f8332ad4caeba941cfe7 depends: - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 - - libsqlite >=3.49.1,<4.0a0 + - libexpat >=2.7.4,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.1,<4.0a0 + - openssl >=3.5.5,<4.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] - size: 15935206 - timestamp: 1741128459438 -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda - build_number: 101 - sha256: b6e7a6f314343926b5a236592272e5014edcda150e14d18d0fb9440d8a185c3f - md5: 5116c74f5e3e77b915b7b72eea0ec946 + size: 15840187 + timestamp: 1772728877265 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.13-h09917c8_100_cp313.conda + build_number: 100 + sha256: b8108d7f83f71fb15fbb4a263406c2065a8990b3d7eba2cbd7a3075b9a6392ba + md5: 7065f7067762c4c2bda1912f18d16239 depends: - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.4,<6.0a0 + - libexpat >=2.7.5,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.48.0,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.1,<4.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - python_abi 3.13.* *_cp313 - tk >=8.6.13,<8.7.0a0 - tzdata - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Python-2.0 purls: [] - size: 16848398 - timestamp: 1739800686310 + size: 16618694 + timestamp: 1775613654892 python_site_packages_path: Lib/site-packages - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.4-h4b44e0e_100_cp314.conda build_number: 100 @@ -7839,49 +6918,51 @@ packages: size: 18055445 timestamp: 1775615317758 python_site_packages_path: Lib/site-packages -- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - sha256: a50052536f1ef8516ed11a844f9413661829aa083304dc624c5925298d078d79 - md5: 5ba79d7c71f03c678c8ead841f347d6e +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 depends: - python >=3.9 - six >=1.5 + - python license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/python-dateutil?source=hash-mapping - size: 222505 - timestamp: 1733215763718 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - sha256: 1b09a28093071c1874862422696429d0d35bd0b8420698003ac004746c5e82a2 - md5: 38e34d2d1d9dca4fb2b9a0a04f604e2c + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 + md5: 23029aae904a2ba587daba708208012f depends: - python >=3.9 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/fastjsonschema?source=hash-mapping - size: 226259 - timestamp: 1733236073335 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.9-hd8ed1ab_1.conda - sha256: d45a5a99ec3ad65d390590905c0d79b6223468d75425d988106473056ddc35e7 - md5: a1a3aa64397603a81615400388409e10 + size: 244628 + timestamp: 1755304154927 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda + sha256: 97327b9509ae3aae28d27217a5d7bd31aff0ab61a02041e9c6f98c11d8a53b29 + md5: 32780d6794b8056b78602103a04e90ef depends: - - cpython 3.12.9.* + - cpython 3.12.13.* - python_abi * *_cp312 license: Python-2.0 purls: [] - size: 45697 - timestamp: 1741128092145 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.2-h4df99d1_101.conda - sha256: 504463a7473d02591a64c7021581682c3ea725c45f8d4680f47c13432f70145b - md5: 5d6ad81a997f8748e813b56b3c277c60 + size: 46449 + timestamp: 1772728979370 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.13-h4df99d1_100.conda + sha256: b2e51d83e5ebeb7e9fde1cde822a60e8564cc9dabd786ad853056afbf708a466 + md5: fd00e4b24ea88093c93f5c9bad27b52f depends: - - cpython 3.13.2.* + - cpython 3.13.13.* - python_abi * *_cp313 license: Python-2.0 purls: [] - size: 47795 - timestamp: 1739800832944 + size: 48536 + timestamp: 1775613791711 - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.4-h4df99d1_100.conda sha256: 36ff7984e4565c85149e64f8206303d412a0652e55cf806dcb856903fa056314 md5: e4e60721757979d01d3964122f674959 @@ -7892,17 +6973,46 @@ packages: purls: [] size: 49806 timestamp: 1775614307464 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - sha256: 4790787fe1f4e8da616edca4acf6a4f8ed4e7c6967aa31b920208fc8f95efcca - md5: a61bf9ec79426938ff785eb69dbb1960 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + sha256: 1c55116c22512cef7b01d55ae49697707f2c1fd829407183c19817e2d300fd8d + md5: 1cd2f3e885162ee1366312bd1b1677fd depends: - - python >=3.6 + - python >=3.10 + - typing_extensions license: BSD-2-Clause license_family: BSD purls: - - pkg:pypi/python-json-logger?source=hash-mapping - size: 13383 - timestamp: 1677079727691 + - pkg:pypi/python-json-logger?source=compressed-mapping + size: 18969 + timestamp: 1777318679482 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py312h5253ce2_0.conda + sha256: 124f40b723fdce04043728fbd3b83d8da908623d2fc031e750a5acafa9934abf + md5: ff391116e9ef3aa2e73a3fa5823b9d6e + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 80343 + timestamp: 1775764040950 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py313h54dd161_0.conda + sha256: 3099881e50d743835cdc1267d9316ac22166bec160984ecaf50fd9827c56adf1 + md5: 8934520d44c6e239bf23e1b97051573c + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=compressed-mapping + size: 79973 + timestamp: 1775764028901 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-librt-0.9.0-py314h0f05182_0.conda sha256: 91e220ec82b60899d522c9ce8d1d3aba727ace155c27ef0ad58efe39417bc94f md5: 3de7ef40dc693af4681d4998297f0052 @@ -7917,6 +7027,32 @@ packages: - pkg:pypi/librt?source=hash-mapping size: 79052 timestamp: 1775764036701 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py312hba6025d_0.conda + sha256: 0dafa8a07fca4ffadc6420a06c52796f96f752fa8cc5134b3c537204c570e309 + md5: b30accf2566324d34a46f5770d910868 + depends: + - python + - __osx >=11.0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 70462 + timestamp: 1775764132923 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py313h22ab4a2_0.conda + sha256: 30a34d6597a0972ae33a260b424fa425017d3dd5f0a50e21d64ca080c87436f8 + md5: 7fe76a7ca9125cc96403c87f967e3223 + depends: + - python + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 70319 + timestamp: 1775764105428 - conda: https://conda.anaconda.org/conda-forge/osx-64/python-librt-0.9.0-py314h0b69929_0.conda sha256: b7414d48343fc05bb5f0bb035f3b66ebf6443b4b0b58d194e2a911680b76711a md5: b5c0be3b92113c71e422833bbd251df1 @@ -7930,6 +7066,34 @@ packages: - pkg:pypi/librt?source=hash-mapping size: 70114 timestamp: 1775764122931 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py312hb3ab3e3_0.conda + sha256: a10f1f4ebfa731e5ef9983c5d139551bc9e18dc15540df5fec85ce8dff2db730 + md5: 5152565a756f10adab676d34914a3302 + depends: + - python + - __osx >=11.0 + - python 3.12.* *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 74280 + timestamp: 1775764144831 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py313h6688731_0.conda + sha256: d8461d6405c97dbde22ee412d6e58fae33e867521614d4ea8487a2f1c662b714 + md5: 8df57ab3318ecdf85b3d61d2782e5787 + depends: + - python + - python 3.13.* *_cp313 + - __osx >=11.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 74689 + timestamp: 1775764138895 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-librt-0.9.0-py314ha14b1ff_0.conda sha256: c68f760966344b6e7e31d7fe2dd90ebdcd4701413cdf2b058da29ff727673884 md5: b409c6b041d02d3795f981f870dc5013 @@ -7944,6 +7108,36 @@ packages: - pkg:pypi/librt?source=hash-mapping size: 74109 timestamp: 1775825547864 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py312he5662c2_0.conda + sha256: ed995317ccd0de59bf18651db01cff25654a8dca6477a6a998c6b277609c060a + md5: 858e236ed3029a2ca4011245ffd75898 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 54146 + timestamp: 1775764113514 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py313h5fd188c_0.conda + sha256: 50130a3b72a984c97a0d770388c69e3c77e680a7f30200a58ceb560dcb56150c + md5: 4e6019e6c7cac87d1a2e13774a54443c + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/librt?source=hash-mapping + size: 54019 + timestamp: 1775764098883 - conda: https://conda.anaconda.org/conda-forge/win-64/python-librt-0.9.0-py314hc5dbbe4_0.conda sha256: 4dd14f2461aa91ec9904a9411025e8c9c91308ea47655d07bc89d3a9d2847f73 md5: 05ea58209be41c7152956f36a496e040 @@ -7959,28 +7153,39 @@ packages: - pkg:pypi/librt?source=hash-mapping size: 51707 timestamp: 1775764106960 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda - build_number: 5 - sha256: d10e93d759931ffb6372b45d65ff34d95c6000c61a07e298d162a3bc2accebb0 - md5: 0424ae29b104430108f5218a66db7260 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 + md5: f6ad7450fc21e00ecc23812baed6d2e4 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=compressed-mapping + size: 146639 + timestamp: 1777068997932 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + build_number: 8 + sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 + md5: c3efd25ac4d74b1584d2f7a57195ddf1 constrains: - python 3.12.* *_cpython license: BSD-3-Clause license_family: BSD purls: [] - size: 6238 - timestamp: 1723823388266 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda - build_number: 5 - sha256: 438225b241c5f9bddae6f0178a97f5870a89ecf927dfca54753e689907331442 - md5: 381bbd2a92c863f640a55b6ff3c35161 + size: 6958 + timestamp: 1752805918820 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 constrains: - python 3.13.* *_cp313 license: BSD-3-Clause license_family: BSD purls: [] - size: 6217 - timestamp: 1723823393322 + size: 7002 + timestamp: 1752805902938 - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda build_number: 8 sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 @@ -7992,115 +7197,45 @@ packages: purls: [] size: 6989 timestamp: 1752805904792 -- conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda - build_number: 5 - sha256: 4da26c7508d5bc5d8621e84dc510284402239df56aab3587a7d217de9d3c806d - md5: c34dd4920e0addf7cfcc725809f25d8e - constrains: - - python 3.12.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6312 - timestamp: 1723823137004 -- conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.13-5_cp313.conda - build_number: 5 - sha256: 075ad768648e88b78d2a94099563b43d3082e7c35979f457164f26d1079b7b5c - md5: 927a2186f1f997ac018d67c4eece90a6 - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6291 - timestamp: 1723823083064 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda - build_number: 5 - sha256: 49d624e4b809c799d2bf257b22c23cf3fc4460f5570d9a58e7ad86350aeaa1f4 - md5: b76f9b1c862128e56ac7aa8cd2333de9 - constrains: - - python 3.12.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6278 - timestamp: 1723823099686 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.13-5_cp313.conda - build_number: 5 - sha256: 4437198eae80310f40b23ae2f8a9e0a7e5c2b9ae411a8621eb03d87273666199 - md5: b8e82d0a5c1664638f87f63cc5d241fb - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6322 - timestamp: 1723823058879 -- conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-5_cp312.conda - build_number: 5 - sha256: 9486662af81a219e96d343449eff242f38d7c5128ced5ce5acf85857265058d6 - md5: e8681f534453af7afab4cd2bc1423eec - constrains: - - python 3.12.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6730 - timestamp: 1723823139725 -- conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - build_number: 5 - sha256: 0c12cc1b84962444002c699ed21e815fb9f686f950d734332a1b74d07db97756 - md5: 44b4fe6f22b57103afb2299935c8b68e - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6716 - timestamp: 1723823166911 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0 - md5: bc8e3267d44011051f2eb14d22fb0960 - depends: - - python >=3.9 - license: MIT - purls: - - pkg:pypi/pytz?source=compressed-mapping - size: 189015 - timestamp: 1742920947249 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py312h275cf98_3.conda - sha256: 68f8781b83942b91dbc0df883f9edfd1a54a1e645ae2a97c48203ff6c2919de3 - md5: 1747fbbdece8ab4358b584698b19c44d +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py312h829343e_1.conda + sha256: a7505522048dad63940d06623f07eb357b9b65510a8d23ff32b99add05aac3a1 + md5: 64cbe4ecbebe185a2261d3f298a60cde depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 license: PSF-2.0 license_family: PSF purls: - pkg:pypi/pywin32?source=hash-mapping - size: 6032183 - timestamp: 1728636767192 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py313h5813708_3.conda - sha256: 0a68b324ea47ae720c62522c5d0bb5ea3e4987e1c5870d6490c7f954fbe14cbe - md5: 7113bd6cfe34e80d8211f7c019d14357 + size: 6684490 + timestamp: 1756487136116 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda + sha256: 6918a8067f296f3c65d43e84558170c9e6c3f4dd735cfe041af41a7fdba7b171 + md5: 2d7b7ba21e8a8ced0eca553d4d53f773 depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 license: PSF-2.0 license_family: PSF purls: - pkg:pypi/pywin32?source=hash-mapping - size: 6060096 - timestamp: 1728636763526 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_0.conda - sha256: 22b901606eda476a19fcc9376a906ef2e16fc6690186bc1d9a213f6c8e93d061 - md5: 1fb4bbe58100be45b37781a367c92fe8 + size: 6713155 + timestamp: 1756487145487 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py312h275cf98_1.conda + sha256: 61cc6c2c712ab4d2b8e7a73d884ef8d3262cb80cc93a4aa074e8b08aa7ddd648 + md5: 66255d136bd0daa41713a334db41d9f0 depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 @@ -8112,14 +7247,14 @@ packages: license_family: MIT purls: - pkg:pypi/pywinpty?source=hash-mapping - size: 215864 - timestamp: 1738661787591 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_0.conda - sha256: 4210038442e3f34d67de9aeab2691fa2a6f80dc8c16ab77d5ecbb2b756e04ff0 - md5: cd1fadcdf82a423c2441a95435eeab3c + size: 215371 + timestamp: 1759557609855 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py314h51f0985_1.conda + sha256: 048e20641da680aedaab285640a2aca56b7b5baf7a18f8f164f2796e13628c1f + md5: dd84e8748bd3c85a5c751b0576488080 depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python >=3.14.0rc3,<3.15.0a0 + - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 @@ -8128,14 +7263,14 @@ packages: license_family: MIT purls: - pkg:pypi/pywinpty?source=hash-mapping - size: 217133 - timestamp: 1738661059040 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda - sha256: 159cba13a93b3fe084a1eb9bda0a07afc9148147647f0d437c3c3da60980503b - md5: cf2485f39740de96e2a7f2bb18ed2fee + size: 216325 + timestamp: 1759557436167 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda + sha256: cb142bfd92f6e55749365ddc244294fa7b64db6d08c45b018ff1c658907bfcbf + md5: 15878599a87992e44c059731771591cb depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - yaml >=0.2.5,<0.3.0a0 @@ -8143,23 +7278,23 @@ packages: license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 206903 - timestamp: 1737454910324 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h8060acc_2.conda - sha256: 6826217690cfe92d6d49cdeedb6d63ab32f51107105d6a459d30052a467037a0 - md5: 50992ba61a8a1f8c2d346168ae1c86df + size: 198293 + timestamp: 1770223620706 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + sha256: ef7df29b38ef04ec67a8888a4aa039973eaa377e8c4b59a7be0a1c50cd7e4ac6 + md5: f256753e840c3cd3766488c9437a8f8b depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 205919 - timestamp: 1737454783637 + - pkg:pypi/pyyaml?source=compressed-mapping + size: 201616 + timestamp: 1770223543730 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d md5: 2035f68f96be30dc60a5dfd7452c7941 @@ -8175,9 +7310,9 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 202391 timestamp: 1770223462836 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312h3520af0_2.conda - sha256: de96d83b805dba03422d39e855fb33cbeedc8827235d6f76407a3b42dc085910 - md5: 4a2d83ac55752681d54f781534ddd209 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda + sha256: d85e3be523b7173a194a66ae05a585ac1e14ccfbe81a9201b8047d6e45f2f7d9 + md5: 9029301bf8a667cf57d6e88f03a6726b depends: - __osx >=10.13 - python >=3.12,<3.13.0a0 @@ -8187,11 +7322,11 @@ packages: license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 193577 - timestamp: 1737454858212 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py313h717bdf5_2.conda - sha256: 27501e9b3b5c6bfabb3068189fd40c650356a258e4a82b0cfe31c60f568dcb85 - md5: b7f2984724531d2233b77c89c54be594 + size: 190417 + timestamp: 1770223755226 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py313h7c6a591_1.conda + sha256: ab5f6c27d24facd1832481ccd8f432c676472d57596a3feaa77880a1462cdb2a + md5: 0eaf6cf9939bb465ee62b17d04254f9e depends: - __osx >=10.13 - python >=3.13,<3.14.0a0 @@ -8201,8 +7336,8 @@ packages: license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 196573 - timestamp: 1737455046063 + size: 192051 + timestamp: 1770223971430 - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py314h10d0514_1.conda sha256: aef010899d642b24de6ccda3bc49ef008f8fddf7bad15ebce9bdebeae19a4599 md5: ebd224b733573c50d2bfbeacb5449417 @@ -8217,9 +7352,9 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 191947 timestamp: 1770226344240 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda - sha256: ad225ad24bfd60f7719709791345042c3cb32da1692e62bd463b084cf140e00d - md5: 68149ed4d4e9e1c42d2ba1f27f08ca96 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda + sha256: 737959262d03c9c305618f2d48c7f1691fb996f14ae420bfd05932635c99f873 + md5: 95a5f0831b5e0b1075bbd80fcffc52ac depends: - __osx >=11.0 - python >=3.12,<3.13.0a0 @@ -8230,11 +7365,11 @@ packages: license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 192148 - timestamp: 1737454886351 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py313ha9b7d5b_2.conda - sha256: 58c41b86ff2dabcf9ccd9010973b5763ec28b14030f9e1d9b371d22b538bce73 - md5: 03a7926e244802f570f25401c25c13bc + size: 187278 + timestamp: 1770223990452 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h65a2061_1.conda + sha256: 950725516f67c9691d81bb8dde8419581c5332c5da3da10c9ba8cbb1698b825d + md5: 5d0c8b92128c93027632ca8f8dc1190f depends: - __osx >=11.0 - python >=3.13,<3.14.0a0 @@ -8245,8 +7380,8 @@ packages: license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 194243 - timestamp: 1737454911892 + size: 188763 + timestamp: 1770224094408 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda sha256: 95f385f9606e30137cf0b5295f63855fd22223a4cf024d306cf9098ea1c4a252 md5: dcf51e564317816cb8d546891019b3ab @@ -8262,38 +7397,38 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 189475 timestamp: 1770223788648 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda - sha256: 76fec03ef7e67e37724873e1f805131fb88efb57f19e9a77b4da616068ef5c28 - md5: ba00a2e5059c1fde96459858537cc8f5 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + sha256: 1cab6cbd6042b2a1d8ee4d6b4ec7f36637a41f57d2f5c5cf0c12b7c4ce6a62f6 + md5: 9f6ebef672522cb9d9a6257215ca5743 depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 181734 - timestamp: 1737455207230 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313hb4c8b1a_2.conda - sha256: 5b496c96e48f495de41525cb1b603d0147f2079f88a8cf061aaf9e17a2fe1992 - md5: d14f685b5d204b023c641b188a8d0d7c + size: 179738 + timestamp: 1770223468771 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_1.conda + sha256: dfaed50de8ee72a51096163b87631921688851001e38c78a841eba1ae8b35889 + md5: c1bdb8dd255c79fb9c428ad25cc6ee54 depends: - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 182783 - timestamp: 1737455202579 + size: 180992 + timestamp: 1770223457761 - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda sha256: a2aff34027aa810ff36a190b75002d2ff6f9fbef71ec66e567616ac3a679d997 md5: 0cd9b88826d0f8db142071eb830bce56 @@ -8310,118 +7445,76 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 181257 timestamp: 1770223460931 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.4.0-py312hbf22597_0.conda - sha256: 65a264837f189b0c69c5431ea8ef44e405c472fedba145b05055f284f08bc663 - md5: fa0ab7d5bee9efbc370e71bcb5da9856 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - zeromq >=4.3.5,<4.4.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 379554 - timestamp: 1743831426292 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-26.4.0-py312h679dbab_0.conda - sha256: 9e89fab2c70a47298e72429b70cbf233d69f16f92c7dcad3b60db2e22afea00d - md5: 7c068120e36588fefecf8e91b1b3ae38 - depends: - - __osx >=10.13 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - zeromq >=4.3.5,<4.4.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 365060 - timestamp: 1743831517482 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-26.4.0-py312hf4875e0_0.conda - sha256: b8b41da0aac8aab5e48e62ff341374f12cd0ace7a59b80f56bc75371aa4796d5 - md5: 1e2a85e9493ad7c892ecbca89a11837c +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + noarch: python + sha256: be66c1f85c3b48137200d62c12d918f4f8ad329423daef04fed292818efd3c28 + md5: 082985717303dab433c976986c674b35 depends: - - __osx >=11.0 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 - zeromq >=4.3.5,<4.4.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/pyzmq?source=hash-mapping - size: 364333 - timestamp: 1743831518152 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-26.4.0-py313he6960b1_0.conda - sha256: 0e0ee756e1fb46456ff398ef77dce595411043836bc47a92d30c9240c9fcef87 - md5: 7f355f62656985be979c4c0003723d0a + size: 211567 + timestamp: 1771716961404 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312h2ac7433_2.conda + noarch: python + sha256: 475d5a751740eef86b4469b73759a42bcf82abb292fde7506081196378552cf3 + md5: 98bc7fb12f6efc9c08eeeac21008a199 depends: + - python - __osx >=11.0 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - libcxx >=19 + - _python_abi3_support 1.* + - cpython >=3.12 - zeromq >=4.3.5,<4.4.0a0 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/pyzmq?source=hash-mapping - size: 369287 - timestamp: 1743831518822 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-26.4.0-py312hd7027bb_0.conda - sha256: 07fbf17632c6300e53550f829f2e10d2c6f68923aa139d0618eaeadf2d0043ae - md5: ccfe948627071c03e36aa46d9e94bf12 + size: 192884 + timestamp: 1771717048943 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + noarch: python + sha256: 2f31f799a46ed75518fae0be75ecc8a1b84360dbfd55096bc2fe8bd9c797e772 + md5: 2f6b79700452ef1e91f45a99ab8ffe5a depends: - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - zeromq >=4.3.5,<4.3.6.0a0 + - python + - libcxx >=19 + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/pyzmq?source=hash-mapping - size: 363177 - timestamp: 1743831815399 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-26.4.0-py313h2100fd5_0.conda - sha256: a4df396a30b654c9bee979c930a982289d610b9d8fc5dd0e0b581251609c9ec0 - md5: b4f6e525ad0101a84c79a2b444432726 + size: 191641 + timestamp: 1771717073430 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda + noarch: python + sha256: d84bcc19a945ca03d1fd794be3e9896ab6afc9f691d58d9c2da514abe584d4df + md5: eb1ec67a70b4d479f7dd76e6c8fe7575 depends: - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - zeromq >=4.3.5,<4.3.6.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/pyzmq?source=hash-mapping - size: 369170 - timestamp: 1743831922949 -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c - md5: 283b96675859b20a825f8fa30f311446 - depends: - - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 282480 - timestamp: 1740379431762 + size: 183235 + timestamp: 1771716967192 - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 md5: d7d95fc8287ea7bf33e0e7116d2b95ec @@ -8434,16 +7527,6 @@ packages: purls: [] size: 345073 timestamp: 1765813471974 -- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - sha256: 53017e80453c4c1d97aaf78369040418dea14cf8f46a2fa999f31bd70b36c877 - md5: 342570f8e02f2f022147a7f841475784 - depends: - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 256712 - timestamp: 1740379577668 - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda sha256: 4614af680aa0920e82b953fece85a03007e0719c3399f13d7de64176874b80d5 md5: eefd65452dfe7cce476a519bece46704 @@ -8455,16 +7538,6 @@ packages: purls: [] size: 317819 timestamp: 1765813692798 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 - md5: 63ef3f6e6d6d5c589e64f11263dc5676 - depends: - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 252359 - timestamp: 1740379663071 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 md5: f8381319127120ce51e081dce4865cf4 @@ -8476,12 +7549,12 @@ packages: purls: [] size: 313930 timestamp: 1765813902568 -- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - sha256: e20909f474a6cece176dfc0dc1addac265deb5fa92ea90e975fbca48085b20c3 - md5: 9140f1c09dd5489549c6a33931b943c7 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab depends: - attrs >=22.2.0 - - python >=3.9 + - python >=3.10 - rpds-py >=0.7.0 - typing_extensions >=4.4.0 - python @@ -8489,25 +7562,8 @@ packages: license_family: MIT purls: - pkg:pypi/referencing?source=hash-mapping - size: 51668 - timestamp: 1737836872415 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - sha256: d701ca1136197aa121bbbe0e8c18db6b5c94acbd041c2b43c70e5ae104e1d8ad - md5: a9b9368f3701a417eac9edbcae7cb737 - depends: - - certifi >=2017.4.17 - - charset-normalizer >=2,<4 - - idna >=2.5,<4 - - python >=3.9 - - urllib3 >=1.21.1,<3 - constrains: - - chardet >=3.0.2,<6 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/requests?source=hash-mapping - size: 58723 - timestamp: 1733217126197 + size: 51788 + timestamp: 1760379115194 - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda sha256: 7f2c24dd3bd3c104a1d2c9a10ead5ed6758b0976b74f972cfe9c19884ccc4241 md5: 9659f587a8ceacc21864260acd02fc67 @@ -8549,6 +7605,19 @@ packages: - pkg:pypi/rfc3986-validator?source=hash-mapping size: 7818 timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 + md5: 7234f99325263a5af6d4cd195035e8f2 + depends: + - python >=3.9 + - lark >=1.2.2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3987-syntax?source=hash-mapping + size: 22913 + timestamp: 1752876729969 - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda sha256: 30f3c04fcfb64c44d821d392a4a0b8915650dbd900c8befc20ade8fde8ec6aa2 md5: 0dc48b4b570931adc8641e55c6c17fe4 @@ -8559,23 +7628,13 @@ packages: - pkg:pypi/roman-numerals?source=hash-mapping size: 13814 timestamp: 1766003022813 -- conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - sha256: 0116a9ca9bf3487e18979b58b2f280116dba55cb53475af7a6d835f7aa133db8 - md5: 5f0f24f8032c2c1bb33f59b75974f5fc - depends: - - python >=3.9 - license: 0BSD OR CC0-1.0 - purls: - - pkg:pypi/roman-numerals-py?source=hash-mapping - size: 13348 - timestamp: 1740240332327 -- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.25.1-py312h680f630_0.conda - sha256: a5b168b991c23ab6d74679a6f5ad1ed87b98ba6c383b5fe41f5f6b335b10d545 - md5: ea8f79edf890d1f9b2f1bd6fbb11be1e +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py312h868fb18_0.conda + sha256: 62f46e85caaba30b459da7dfcf3e5488ca24fd11675c33ce4367163ab191a42c + md5: 3ffc5a3572db8751c2f15bacf6a0e937 depends: - python - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python_abi 3.12.* *_cp312 constrains: - __glibc >=2.17 @@ -8583,125 +7642,116 @@ packages: license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 391950 - timestamp: 1747837859184 -- conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.25.1-py312haba3716_0.conda - sha256: 26728fe74ed4a300651ae901b783fb7bddcabc7b27c3db2c62f8b2dfc64d9f01 - md5: d66be2aa77f9a1acd02a5ac59c9f5294 + size: 383750 + timestamp: 1764543174231 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + sha256: 076d26e51c62c8ecfca6eb19e3c1febdd7632df1990a7aa53da5df5e54482b1c + md5: 779e3307a0299518713765b83a36f4b1 depends: - python - - __osx >=10.13 - - python_abi 3.12.* *_cp312 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 constrains: - - __osx >=10.13 + - __glibc >=2.17 license: MIT license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 370933 - timestamp: 1747837775787 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.25.1-py312hd3c0895_0.conda - sha256: 9a2f4a7340a73bc618550738bdf22835325d4ce88a98e26a55e2b5f6e873f306 - md5: 3b50fde83777a12d5bf4511d9baecc98 + size: 383230 + timestamp: 1764543223529 +- conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.30.0-py312h8a6388b_0.conda + sha256: 3df6f3ad2697f5250d38c37c372b77cc2702b0c705d3d3a231aae9dc9f2eec62 + md5: 9adbe03b6d1b86cab37fb37709eb4e38 depends: - python - - python 3.12.* *_cpython - - __osx >=11.0 + - __osx >=10.13 - python_abi 3.12.* *_cp312 constrains: - - __osx >=11.0 + - __osx >=10.13 license: MIT license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 360032 - timestamp: 1747837743255 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.25.1-py313hf3ab51e_0.conda - sha256: 00c61b2054307fb60feaeb1d21515acb6ee917ff73cfc622fef55d4c24a32767 - md5: 1df95fc541f0881e89dc4a52bd53b9ee + size: 370624 + timestamp: 1764543158734 +- conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.30.0-py313hcc225dc_0.conda + sha256: 8955e67a30f44fbfd390374ba27f445b9e56818b023ccb8fe8f0cd00bec03caa + md5: 7c8790b86262342a2c4f4c9709cf61ae depends: - python - - python 3.13.* *_cp313 - - __osx >=11.0 + - __osx >=10.13 - python_abi 3.13.* *_cp313 constrains: - - __osx >=11.0 + - __osx >=10.13 license: MIT license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 360004 - timestamp: 1747837756479 -- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.25.1-py312h8422cdd_0.conda - sha256: dfea71a35d7d5eb348893e24136ce6fb1004fc9402eaafae441fa61887638764 - md5: 30d51df2ebcc324cce80fa6a317df920 + size: 370868 + timestamp: 1764543169321 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py312h6ef9ec0_0.conda + sha256: ea06f6f66b1bea97244c36fd2788ccd92fd1fb06eae98e469dd95ee80831b057 + md5: a7cfbbdeb93bb9a3f249bc4c3569cd4c depends: - python - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 + - __osx >=11.0 + - python 3.12.* *_cpython - python_abi 3.12.* *_cp312 + constrains: + - __osx >=11.0 license: MIT license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 252939 - timestamp: 1747837730306 -- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.25.1-py313ha8a9a3c_0.conda - sha256: f9a4e4e57fb6b6f82a70f533edc5b2be1084770b6cd99913713ab856886da7d9 - md5: 16d91b61a62fa344b9c1200b13925fbd + size: 358853 + timestamp: 1764543161524 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.30.0-py314haad56a0_0.conda + sha256: e161dd97403b8b8a083d047369a5cf854557dba1204d29e2f0250f5ac4403925 + md5: 76a4f88d1b7748c477abf3c341edc64c depends: - python - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - python_abi 3.13.* *_cp313 + - __osx >=11.0 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 license: MIT license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 252641 - timestamp: 1747837734433 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py312hf79aa60_0.conda - sha256: 72e1934499126cb9a3a5aa00e535fc430617206f0ecd8f34f5afd6bdb572a6a8 - md5: ce118d87ae26bd6204ac95aa7d7bd32e + size: 350976 + timestamp: 1764543169524 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py312hdabe01f_0.conda + sha256: faad05e6df2fc15e3ae06fdd71a36e17ff25364777aa4c40f2ec588740d64091 + md5: 2c51baeda0a355b0a5e7b6acb28cf02d depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.12,<3.13.0a0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.12.* *_cp312 - constrains: - - __glibc >=2.17 license: MIT license_family: MIT purls: - - pkg:pypi/ruff?source=hash-mapping - size: 8907135 - timestamp: 1742584315193 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.11.2-py313hfe82de2_0.conda - sha256: f25deaa5163f8baf0090908fae0cfeeffda650306f9bfb5ec5143bda7edc2a58 - md5: 520be555c706dbc4385f82d1cb9dc41c + - pkg:pypi/rpds-py?source=hash-mapping + size: 243577 + timestamp: 1764543069837 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.30.0-py314h9f07db2_0.conda + sha256: e4435368c5c25076dc0f5918ba531c5a92caee8e0e2f9912ef6810049cf00db2 + md5: e86531e278ad304438e530953cd55d14 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - constrains: - - __glibc >=2.17 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 license: MIT license_family: MIT purls: - - pkg:pypi/ruff?source=hash-mapping - size: 8872400 - timestamp: 1742584319600 + - pkg:pypi/rpds-py?source=hash-mapping + size: 235780 + timestamp: 1764543046065 - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.11-h7805a7d_0.conda noarch: python sha256: cdbe0e611cf6abfea4d0a8d31721cdd357987ebc4521392638d7b57169422968 @@ -8718,38 +7768,6 @@ packages: - pkg:pypi/ruff?source=compressed-mapping size: 9327937 timestamp: 1776378777189 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py312ha54e1fc_0.conda - sha256: 972a8192ec8f73d20f8e665c4cafd5aeefdc8bd8adbfdb83fc1c2bd02598d3cb - md5: dcc943dc0c72dbd74524c0e410243204 - depends: - - __osx >=10.13 - - libcxx >=18 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - constrains: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 8171469 - timestamp: 1742584863664 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.11.2-py313h2e013d6_0.conda - sha256: 57502cc6d8485cde43b56ffaa78474e55865e826a929340b190d992770dcacb2 - md5: 1d4fc6acbbb5ef785e3553dc986a5fe0 - depends: - - __osx >=10.13 - - libcxx >=18 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - constrains: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 8171146 - timestamp: 1742584829512 - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.11-h16586dd_0.conda noarch: python sha256: 4b9adce4d8d99bf5f193a8bf3b2aaa91f3b65d88fd610f61a6330120704eacaf @@ -8765,40 +7783,6 @@ packages: - pkg:pypi/ruff?source=hash-mapping size: 9350619 timestamp: 1776378920511 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py312h31a5b27_0.conda - sha256: d8ec16cdee63ab6279f2f174344563e0eef5597167bfe3b1d4001b5c9f140187 - md5: 04650bb002095ba4918311d035c167b2 - depends: - - __osx >=11.0 - - libcxx >=18 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 7802579 - timestamp: 1742585199918 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.11.2-py313h35210b4_0.conda - sha256: 70f8c750b2ea339d54679922cca6d61cee8b54ada05d2245ceac29d46a8ce594 - md5: 043f4d37ec2eb12e2ecbef8e5ac381b2 - depends: - - __osx >=11.0 - - libcxx >=18 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 7801806 - timestamp: 1742584905314 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.11-hc5c3a1d_0.conda noarch: python sha256: 2c8d24c58059cc1ed590276591634482fe921d2542957323caaa21e053cf6971 @@ -8814,36 +7798,6 @@ packages: - pkg:pypi/ruff?source=compressed-mapping size: 8510514 timestamp: 1776378932502 -- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py312hc33538c_0.conda - sha256: 3444f42e218faa07de917de7aeec08a16a3dba9a855aabe6f72ea721792edc3d - md5: ab488dc1f8101c17d0fc4ff938860cec - depends: - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 7911833 - timestamp: 1742585281771 -- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.11.2-py313he8c32b4_0.conda - sha256: bb59f57e745e54452ccf7f8ef0be7ca3b2939b9da54cb4933f21c6bee92177aa - md5: 6aee141098589e299c99e8c5fb3de62e - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=hash-mapping - size: 7912086 - timestamp: 1742585732752 - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.11-h02f8532_0.conda noarch: python sha256: 29b1d24ad55d68abe04ff7911107344e63d3b76ae54f58c52a2a74fbf8a53c4c @@ -8859,93 +7813,86 @@ packages: - pkg:pypi/ruff?source=compressed-mapping size: 9828825 timestamp: 1776378829267 -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - sha256: 00926652bbb8924e265caefdb1db100f86a479e8f1066efe395d5552dde54d02 - md5: 938c8de6b9de091997145b3bf25cdbf9 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh5552912_1.conda + sha256: 8fc024bf1a7b99fc833b131ceef4bef8c235ad61ecb95a71a6108be2ccda63e8 + md5: b70e2d44e6aa2beb69ba64206a16e4c6 depends: - - __linux - - python >=3.9 + - __osx + - pyobjc-framework-cocoa + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/send2trash?source=hash-mapping - size: 22736 - timestamp: 1733322148326 -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda - sha256: 5282eb5b462502c38df8cb37cd1542c5bbe26af2453a18a0a0602d084ca39f53 - md5: e67b1b1fa7a79ff9e8e326d0caf55854 + size: 22519 + timestamp: 1770937603551 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyh6dadd2b_1.conda + sha256: 305446a0b018f285351300463653d3d3457687270e20eda37417b12ee386ef76 + md5: 6ac53f3fff2c416d63511843a04646fa depends: - - __osx - - pyobjc-framework-cocoa - - python >=3.9 + - __win + - pywin32 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/send2trash?source=hash-mapping - size: 23100 - timestamp: 1733322309409 -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_1.conda - sha256: ba8b93df52e0d625177907852340d735026c81118ac197f61f1f5baea19071ad - md5: e6a4e906051565caf5fdae5b0415b654 + size: 22864 + timestamp: 1770937641143 +- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_1.conda + sha256: 59656f6b2db07229351dfb3a859c35e57cc8e8bcbc86d4e501bff881a6f771f1 + md5: 28eb91468df04f655a57bcfbb35fc5c5 depends: - - __win - - python >=3.9 - - pywin32 + - __linux + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/send2trash?source=hash-mapping - size: 23359 - timestamp: 1733322590167 -- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - sha256: 972560fcf9657058e3e1f97186cc94389144b46dbdf58c807ce62e83f977e863 - md5: 4de79c071274a53dcaf2a8c749d1499e + size: 24108 + timestamp: 1770937597662 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1 + md5: 8e194e7b992f99a5015edbd4ebd38efd depends: - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/setuptools?source=hash-mapping - size: 748788 - timestamp: 1748804951958 + size: 639697 + timestamp: 1773074868565 - pypi: https://files.pythonhosted.org/packages/48/72/920ed1224c94a8a5a69e6c1275ac7fe4eb911ba8feffddf469f1629d47f3/simpy-4.1.1-py3-none-any.whl name: simpy version: 4.1.1 sha256: 7c5ae380240fd2238671160e4830956f8055830a8317edf5c05e495b3823cd88 requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - sha256: 41db0180680cc67c3fa76544ffd48d6a5679d96f4b71d7498a759e94edc9a2db - md5: a451d576819089b0d672f18768be0f65 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 depends: - python >=3.9 + - python license: MIT license_family: MIT purls: - pkg:pypi/six?source=hash-mapping - size: 16385 - timestamp: 1733381032766 -- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - sha256: c2248418c310bdd1719b186796ae50a8a77ce555228b6acd32768e2543a15012 - md5: bf7a226e58dfb8346c70df36065d86c9 + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad + md5: 03fe290994c5e4ec17293cfb6bdce520 depends: - - python >=3.9 + - python >=3.10 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/sniffio?source=hash-mapping - size: 15019 - timestamp: 1733244175724 -- conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - sha256: a0fd916633252d99efb6223b1050202841fa8d2d53dacca564b0ed77249d3228 - md5: 4d22a9315e78c6827f806065957d566e - depends: - - python >=2 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/snowballstemmer?source=hash-mapping - size: 58824 - timestamp: 1637143137377 + size: 15698 + timestamp: 1762941572482 - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda sha256: 17007a4cfbc564dc3e7310dcbe4932c6ecb21593d4fec3c68610720f19e73fb2 md5: 755cf22df8693aa0d1aec1c123fa5863 @@ -8957,56 +7904,17 @@ packages: - pkg:pypi/snowballstemmer?source=hash-mapping size: 73009 timestamp: 1747749529809 -- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - sha256: 54ae221033db8fbcd4998ccb07f3c3828b4d77e73b0c72b18c1d6a507059059c - md5: 3f144b2c34f8cb5a9abd9ed23a39c561 - depends: - - python >=3.8 - license: MIT - license_family: MIT - purls: - - pkg:pypi/soupsieve?source=hash-mapping - size: 36754 - timestamp: 1693929424267 - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda sha256: 23b71ecf089967d2900126920e7f9ff18cdcef82dbff3e2f54ffa360243a17ac - md5: 18de09b20462742fe093ba39185d9bac - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/soupsieve?source=hash-mapping - size: 38187 - timestamp: 1769034509657 -- conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - sha256: 995f58c662db0197d681fa345522fd9e7ac5f05330d3dff095ab2f102e260ab0 - md5: f7af826063ed569bb13f7207d6f949b0 - depends: - - alabaster >=0.7.14 - - babel >=2.13 - - colorama >=0.4.6 - - docutils >=0.20,<0.22 - - imagesize >=1.3 - - jinja2 >=3.1 - - packaging >=23.0 - - pygments >=2.17 - - python >=3.11 - - requests >=2.30.0 - - roman-numerals-py >=1.0.0 - - snowballstemmer >=2.2 - - sphinxcontrib-applehelp >=1.0.7 - - sphinxcontrib-devhelp >=1.0.6 - - sphinxcontrib-htmlhelp >=2.0.6 - - sphinxcontrib-jsmath >=1.0.1 - - sphinxcontrib-qthelp >=1.0.6 - - sphinxcontrib-serializinghtml >=1.1.9 - license: BSD-2-Clause - license_family: BSD + md5: 18de09b20462742fe093ba39185d9bac + depends: + - python >=3.10 + license: MIT + license_family: MIT purls: - - pkg:pypi/sphinx?source=hash-mapping - size: 1424416 - timestamp: 1740956642838 + - pkg:pypi/soupsieve?source=hash-mapping + size: 38187 + timestamp: 1769034509657 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda sha256: 035ca4b17afca3d53650380dd94c564555b7ec2b4f8818111f98c15c7a991b7b md5: aabfbc2813712b71ba8beb217a978498 @@ -9106,18 +8014,6 @@ packages: - pkg:pypi/sphinxcontrib-serializinghtml?source=hash-mapping size: 28669 timestamp: 1733750596111 -- conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.14.0-pyhd8ed1ab_1.conda - sha256: 927e3034aa6cc7f402eeb62b8aeb5bde86f829f4c9551ca564653990ee950519 - md5: 39e5de74df82fc9c269849534d45cbef - depends: - - pathspec >=0.9.0 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ssort?source=hash-mapping - size: 28731 - timestamp: 1735848317236 - conda: https://conda.anaconda.org/conda-forge/noarch/ssort-0.16.0-pyhcf101f3_0.conda sha256: fbbf7244ab5e0c369073c61d34ddcc5999130e620b4bcae3edeb15e262fa52a7 md5: 77f7731cbc8b1e17c86a9756fc914c7f @@ -9145,48 +8041,36 @@ packages: - pkg:pypi/stack-data?source=hash-mapping size: 26988 timestamp: 1733569565672 -- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - sha256: b300557c0382478cf661ddb520263508e4b3b5871b471410450ef2846e8c352c - md5: efba281bbdae5f6b0a1d53c6d4a97c93 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda + sha256: b375e8df0d5710717c31e7c8e93c025c37fa3504aea325c7a55509f64e5d4340 + md5: e43ca10d61e55d0a8ec5d8c62474ec9e depends: - - __linux - - ptyprocess - - python >=3.8 + - __win + - pywinpty >=1.1.0 + - python >=3.10 - tornado >=6.1.0 + - python license: BSD-2-Clause license_family: BSD purls: - pkg:pypi/terminado?source=hash-mapping - size: 22452 - timestamp: 1710262728753 -- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda - sha256: 4daae56fc8da17784578fbdd064f17e3b3076b394730a14119e571707568dc8a - md5: 00b54981b923f5aefcd5e8547de056d5 + size: 23665 + timestamp: 1766513806974 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb + md5: 17b43cee5cc84969529d5d0b0309b2cb depends: - - __osx + - __unix - ptyprocess - - python >=3.8 - - tornado >=6.1.0 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/terminado?source=hash-mapping - size: 22717 - timestamp: 1710265922593 -- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda - sha256: 8cb078291fd7882904e3de594d299c8de16dd3af7405787fce6919a385cfc238 - md5: 4abd500577430a942a995fd0d09b76a2 - depends: - - __win - - python >=3.8 - - pywinpty >=1.1.0 + - python >=3.10 - tornado >=6.1.0 + - python license: BSD-2-Clause license_family: BSD purls: - pkg:pypi/terminado?source=hash-mapping - size: 22883 - timestamp: 1710262943966 + size: 24749 + timestamp: 1766513766867 - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 md5: f1acf5fdefa8300de697982bcb1761c9 @@ -9213,27 +8097,6 @@ packages: purls: [] size: 3301196 timestamp: 1769460227866 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e - md5: d453b98d9c83e71da0741bb0ff4d76bc - depends: - - libgcc-ng >=12 - - libzlib >=1.2.13,<2.0.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3318875 - timestamp: 1699202167581 -- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - sha256: 30412b2e9de4ff82d8c2a7e5d06a15f4f4fef1809a72138b6ccb53a33b26faf5 - md5: bf830ba5afc507c6232d4ef0fb1a882d - depends: - - libzlib >=1.2.13,<2.0.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3270220 - timestamp: 1699202389792 - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda sha256: 7f0d9c320288532873e2d8486c331ec6d87919c9028208d3f6ac91dc8f99a67b md5: 6e6efb7463f8cef69dbcb4c2205bf60e @@ -9256,28 +8119,6 @@ packages: purls: [] size: 3127137 timestamp: 1769460817696 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 - md5: b50a57ba89c32b62428b71a875291c9b - depends: - - libzlib >=1.2.13,<2.0.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3145523 - timestamp: 1699202432999 -- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - sha256: 2c4e914f521ccb2718946645108c9bd3fc3216ba69aea20c2c3cedbd8db32bb1 - md5: fc048363eb8f03cd1737600a5d08aafe - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: TCL - license_family: BSD - purls: [] - size: 3503410 - timestamp: 1699202577803 - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 md5: 0481bfd9814bf525bd4b3ee4b51494c4 @@ -9290,29 +8131,6 @@ packages: purls: [] size: 3526350 timestamp: 1769460339384 -- conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda - sha256: 34f3a83384ac3ac30aefd1309e69498d8a4aa0bf2d1f21c645f79b180e378938 - md5: b0dd904de08b7db706167240bf37b164 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/toml?source=hash-mapping - size: 22132 - timestamp: 1734091907682 -- conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.0.1-pyhd8ed1ab_1.conda - sha256: 47bb3e9bc24d6aa01110e62c273e54364f875ab6fdf26e3a588510d92fb01122 - md5: 31a4c0159fbec114d19c5e8ab3a4cfb4 - depends: - - python >=3.9 - - tomli >=2.0.2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/toml-fmt-common?source=hash-mapping - size: 13323 - timestamp: 1735486723150 - conda: https://conda.anaconda.org/conda-forge/noarch/toml-fmt-common-1.3.2-pyhcf101f3_0.conda sha256: 84df8bf7b7359aad0bae13c8ffd8b2bb869bdc434d660c51620283f7339e7ba8 md5: efa7b8a03b79b5a249ca23821dad2e3a @@ -9326,17 +8144,6 @@ packages: - pkg:pypi/toml-fmt-common?source=hash-mapping size: 15589 timestamp: 1774040339912 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - sha256: 18636339a79656962723077df9a56c0ac7b8a864329eb8f847ee3d38495b863e - md5: ac944244f1fed2eb49bae07193ae8215 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomli?source=hash-mapping - size: 19167 - timestamp: 1733256819729 - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd md5: b5325cf06a000c5b14970462ff5e4d58 @@ -9349,36 +8156,63 @@ packages: - pkg:pypi/tomli?source=hash-mapping size: 21561 timestamp: 1774492402955 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda - sha256: c96be4c8bca2431d7ad7379bad94ed6d4d25cd725ae345540a531d9e26e148c9 - md5: c532a6ee766bed75c4fa0c39e959d132 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda + sha256: 4629b1c9139858fb08bb357df917ffc12e4d284c57ff389806bb3ae476ef4e0a + md5: 2b37798adbc54fd9e591d24679d2133a depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: Apache-2.0 license_family: Apache purls: + - pkg:pypi/tornado?source=compressed-mapping + size: 859665 + timestamp: 1774358032165 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py313h07c4f96_0.conda + sha256: 9e8497e1ecca77d03c6be2d3b5f901dfe0ab99686af4fb94ab418b7d449ac547 + md5: 6c0b0ae017b5bfd9c8d718217efd8f14 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: - pkg:pypi/tornado?source=hash-mapping - size: 850902 - timestamp: 1748003427956 -- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.1-py312h01d7ebd_0.conda - sha256: 6e97d6785c466ddd0fe3dad3aa54db6434824bcab40f7490e90943018560bf67 - md5: 62b3f3d78cb285b2090024e2a1e795f7 + size: 882996 + timestamp: 1774358035145 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda + sha256: da46b42ce6413f6fd03195507df57c8149cc04e043cdbf4d06f7cae236a5445c + md5: af855ebae119a5ff8902ef7a8e200303 depends: - - __osx >=10.13 + - __osx >=11.0 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/tornado?source=hash-mapping - size: 850340 - timestamp: 1748003643552 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py312hea69d52_0.conda - sha256: 02835bf9f49a7c6f73622614be67dc20f9b5c2ce9f663f427150dc0579007daa - md5: 375a5a90946ff09cd98b9cf5b833023c + size: 857717 + timestamp: 1774358322837 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py313hf59fe81_0.conda + sha256: 56aa23963d1b239505503292be6b7626a94bb37264cbeeada85c224615c23c0a + md5: 0e435c1a2ef13ac7b12d7cffe408d7af + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 882579 + timestamp: 1774358602446 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda + sha256: 29edd36311b4a810a9e6208437bdbedb28c9ac15221caf812cb5c5cf48375dca + md5: 02cce5319b0f1317d9642dcb2e475379 depends: - __osx >=11.0 - python >=3.12,<3.13.0a0 @@ -9388,52 +8222,52 @@ packages: license_family: Apache purls: - pkg:pypi/tornado?source=hash-mapping - size: 851614 - timestamp: 1748003575892 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py313h90d716c_0.conda - sha256: 29c623cfb1f9ea7c1d865cf5f52ae6faa6497ceddbe7841ae27901a21f8cf79f - md5: 1ab3bef3e9aa0bba9eee2dfbedab1dba + size: 859155 + timestamp: 1774358568476 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py314h6c2aa35_0.conda + sha256: 4ccc4a20d676c0ba85adee9c99015bec7f5b685df0cf8006e34573f1d6c2ce75 + md5: 3f81f8b2fe2c26a82c0abf57ab2b9610 depends: - __osx >=11.0 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/tornado?source=hash-mapping - size: 874352 - timestamp: 1748003547444 -- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py312h4389bb4_0.conda - sha256: cec4ab331788122f7f01dd02f57f8e21d9ae14553dedd6389d7dfeceb3592399 - md5: 06b156bbbe1597eb5ea30b931cadaa32 + size: 910845 + timestamp: 1774358965067 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py312he06e257_0.conda + sha256: 1220c986664e9e8662e660dc64dd97ed823926b1ba05175771408cf1d6a46dd2 + md5: c6c66a64da3d2953c83ed2789a7f4bdb depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: Apache purls: - - pkg:pypi/tornado?source=hash-mapping - size: 853357 - timestamp: 1748003925528 -- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py313ha7868ed_0.conda - sha256: 4d5511a98b3450157f40479eb3d00bbf3c4741c97149e2914258f71715c5cb47 - md5: a6a7c54e5dfc3bfad645e714cc14854c + - pkg:pypi/tornado?source=compressed-mapping + size: 859726 + timestamp: 1774358173994 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.5-py314h5a2d7ad_0.conda + sha256: 49d64837dd02475903479ca47b82669bd6c9f7e6afde61860c6f3f2bd57d8a03 + md5: 87b1215adf7f0ba1fb9250af9fc668e1 depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/tornado?source=hash-mapping - size: 878044 - timestamp: 1748003914685 + size: 914835 + timestamp: 1774358183098 - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 md5: 019a7385be9af33791c989871317e1ed @@ -9445,27 +8279,6 @@ packages: - pkg:pypi/traitlets?source=hash-mapping size: 110051 timestamp: 1733367480074 -- conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250516-pyhd8ed1ab_0.conda - sha256: 0fb78e97cad71ebf911958bf97777ec958a64a4621615a4dcc3ffb52cda7c6d0 - md5: e3465397ca4b5b60ba9fbc92ef0672f9 - depends: - - python >=3.9 - license: Apache-2.0 AND MIT - purls: - - pkg:pypi/types-python-dateutil?source=hash-mapping - size: 22634 - timestamp: 1747417327584 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - noarch: python - sha256: c8e9c1c467b5f960b627d7adc1c65fece8e929a3de89967e91ef0f726422fd32 - md5: b6a408c64b78ec7b779a3e5c7a902433 - depends: - - typing_extensions 4.12.2 pyha770c72_1 - license: PSF-2.0 - license_family: PSF - purls: [] - size: 10075 - timestamp: 1733188758872 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c md5: edd329d7d3a4ab45dcf905899a7a6115 @@ -9476,17 +8289,6 @@ packages: purls: [] size: 91383 timestamp: 1756220668932 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - sha256: 337be7af5af8b2817f115b3b68870208b30c31d3439bec07bfb2d8f4823e3568 - md5: d17f13df8b65464ca316cbc000a3cb64 - depends: - - python >=3.9 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/typing-extensions?source=hash-mapping - size: 39637 - timestamp: 1733188758212 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 md5: 0caa1af407ecff61170c9437a808404d @@ -9510,13 +8312,6 @@ packages: - pkg:pypi/typing-utils?source=hash-mapping size: 15183 timestamp: 1733331395943 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 - md5: 4222072737ccff51314b5ece9c7d6f5a - license: LicenseRef-Public-Domain - purls: [] - size: 122968 - timestamp: 1742727099393 - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c md5: ad659d0a2b3e47e38d829aa8cad2d610 @@ -9524,15 +8319,6 @@ packages: purls: [] size: 119135 timestamp: 1767016325805 -- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - sha256: db8dead3dd30fb1a032737554ce91e2819b43496a0db09927edf01c32b577450 - md5: 6797b005cd0f439c4c5c9ac565783700 - constrains: - - vs2015_runtime >=14.29.30037 - license: LicenseRef-MicrosoftWindowsSDK10 - purls: [] - size: 559710 - timestamp: 1728377334097 - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 md5: 71b24316859acd00bdb8b38f5e2ce328 @@ -9546,7 +8332,7 @@ packages: - pypi: ./ name: upstage-des version: 1.0.0 - sha256: 966dd9a5d916b6460463e2944d9cdc2af39ea6e67b34eb7e4bdb13aa0aaac16c + sha256: dd3fcc09522a801ee403ec6cd0b42d715a8eca763c122fc43fdc9adea0d75373 requires_dist: - simpy>=4 - myst-parser ; extra == 'docs' @@ -9574,21 +8360,6 @@ packages: - pkg:pypi/uri-template?source=hash-mapping size: 23990 timestamp: 1733323714454 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - sha256: 114919ffa80c328127dab9c8e7a38f9d563c617691fb81fccb11c1e86763727e - md5: 32674f8dbfb7b26410ed580dd3c10a29 - depends: - - brotli-python >=1.0.9 - - h2 >=4,<5 - - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.9 - - zstandard >=0.18.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/urllib3?source=hash-mapping - size: 100102 - timestamp: 1734859520452 - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda sha256: af641ca7ab0c64525a96fd9ad3081b0f5bcf5d1cbb091afb3f6ed5a9eee6111a md5: 9272daa869e03efe68833e3dc7a02130 @@ -9616,30 +8387,6 @@ packages: purls: [] size: 19356 timestamp: 1767320221521 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hbf610ac_25.conda - sha256: 908bd027ce305bd585e1662efba3085be7376b2d81bdc84d7c39311298f28de8 - md5: 632e2d558d7e9f7762b03366d615fbd3 - depends: - - vc14_runtime >=14.42.34438 - track_features: - - vc14 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 18121 - timestamp: 1743121401886 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.42.34438-hfd919c2_25.conda - sha256: 801a64ecc6c10de4b572b411993dc133d39075e3849e1e8feeebe2381aea4986 - md5: cce6df89e439de7ed5f568b0f3eac593 - depends: - - ucrt >=10.0.20348.0 - constrains: - - vs2015_runtime 14.42.34438.* *_25 - license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime - license_family: Proprietary - purls: [] - size: 751016 - timestamp: 1743121397932 - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 md5: 37eb311485d2d8b2c419449582046a42 @@ -9665,38 +8412,28 @@ packages: purls: [] size: 115235 timestamp: 1767320173250 -- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.42.34438-h7142326_25.conda - sha256: 688f9bd904754bf5ddda7c602e43ee544a2f23c005bb6a6f4603082b7f998285 - md5: 946b28e9f15703e4afc591a6183e18a2 - depends: - - vc14_runtime >=14.42.34438 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 18111 - timestamp: 1743121402245 -- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - sha256: f21e63e8f7346f9074fd00ca3b079bd3d2fa4d71f1f89d5b6934bf31446dc2a5 - md5: b68980f2495d096e71c7fd9d7ccf63e6 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + sha256: 1ee2d8384972ecbf8630ce8a3ea9d16858358ad3e8566675295e66996d5352da + md5: eb9538b8e55069434a18547f43b96059 depends: - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - - pkg:pypi/wcwidth?source=hash-mapping - size: 32581 - timestamp: 1733231433877 -- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda - sha256: 08315dc2e61766a39219b2d82685fc25a56b2817acf84d5b390176080eaacf99 - md5: b49f7b291e15494aafb0a7d74806f337 + - pkg:pypi/wcwidth?source=compressed-mapping + size: 82917 + timestamp: 1777744489106 +- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 + md5: 6639b6b0d8b5a284f027a2003669aa65 depends: - - python >=3.9 + - python >=3.10 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/webcolors?source=hash-mapping - size: 18431 - timestamp: 1733359823938 + size: 18987 + timestamp: 1761899393153 - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 md5: 2841eb5bfc75ce15e9a0054b98dcd64d @@ -9708,17 +8445,17 @@ packages: - pkg:pypi/webencodings?source=hash-mapping size: 15496 timestamp: 1733236131358 -- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - sha256: 1dd84764424ffc82030c19ad70607e6f9e3b9cb8e633970766d697185652053e - md5: 84f8f77f0a9c6ef401ee96611745da8f +- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 + md5: 2f1ed718fcd829c184a6d4f0f2e07409 depends: - - python >=3.9 + - python >=3.10 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/websocket-client?source=hash-mapping - size: 46718 - timestamp: 1733157432924 + size: 61391 + timestamp: 1759928175142 - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f md5: 46e441ba871f524e2b067929da3051c2 @@ -9748,24 +8485,6 @@ packages: purls: [] size: 85189 timestamp: 1753484064210 -- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 - md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae - depends: - - libgcc-ng >=9.4.0 - license: MIT - license_family: MIT - purls: [] - size: 89141 - timestamp: 1641346969816 -- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2 - sha256: 5301417e2c8dea45b401ffee8df3957d2447d4ce80c83c5ff151fc6bfe1c4148 - md5: d7e08fcf8259d742156188e8762b4d20 - license: MIT - license_family: MIT - purls: [] - size: 84237 - timestamp: 1641347062780 - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda sha256: a335161bfa57b64e6794c3c354e7d49449b28b8d8a7c4ed02bf04c3f009953f9 md5: a645bb90997d3fc2aea0adf6517059bd @@ -9776,14 +8495,6 @@ packages: purls: [] size: 79419 timestamp: 1753484072608 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 - sha256: 93181a04ba8cfecfdfb162fc958436d868cc37db504c58078eab4c1a3e57fbb7 - md5: 4bb3f014845110883a3c5ee811fd84b4 - license: MIT - license_family: MIT - purls: [] - size: 88016 - timestamp: 1641347076660 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac md5: 78a0fe9e9c50d2c381e8ee47e3ea437d @@ -9809,202 +8520,72 @@ packages: purls: [] size: 63944 timestamp: 1753484092156 -- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - sha256: 4e2246383003acbad9682c7c63178e2e715ad0eb84f03a8df1fbfba455dfedc5 - md5: adbfb9f45d1004a26763652246a33764 - depends: - - vc >=14.1,<15.0a0 - - vs2015_runtime >=14.16.27012 - license: MIT - license_family: MIT - purls: [] - size: 63274 - timestamp: 1641347623319 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - sha256: a4dc72c96848f764bb5a5176aa93dd1e9b9e52804137b99daeebba277b31ea10 - md5: 3947a35e916fcc6b9825449affbf4214 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + sha256: 325d370b28e2b9cc1f765c5b4cdb394c91a5d958fbd15da1a14607a28fee09f6 + md5: 755b096086851e1193f3b10347415d7c depends: + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 335400 - timestamp: 1731585026517 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h7130eaa_7.conda - sha256: b932dce8c9de9a8ffbf0db0365d29677636e599f7763ca51e554c43a0c5f8389 - md5: 6a0a76cd2b3d575e1b7aaeb283b9c3ed + size: 311150 + timestamp: 1772476812121 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h27d9b8f_10.conda + sha256: c7265cc5184897358af8b87c614288bc79645ef4340e01c2cd8469078dc56007 + md5: 1a774dcaff94c2dd98451a26a46714b8 depends: - - __osx >=10.13 - - krb5 >=1.21.3,<1.22.0a0 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 + - libcxx >=19 + - __osx >=11.0 + - libsodium >=1.0.21,<1.0.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 292112 - timestamp: 1731585246902 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda - sha256: 9e585569fe2e7d3bea71972cd4b9f06b1a7ab8fa7c5139f92a31cbceecf25a8a - md5: f7e6b65943cb73bce0143737fded08f1 + size: 260841 + timestamp: 1772476936933 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h4818236_10.conda + sha256: 2705360c72d4db8de34291493379ffd13b09fd594d0af20c9eefa8a3f060d868 + md5: e85dcd3bde2b10081cdcaeae15797506 depends: - __osx >=11.0 - - krb5 >=1.21.3,<1.22.0a0 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 + - libcxx >=19 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 281565 - timestamp: 1731585108039 -- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_7.conda - sha256: 15cc8e2162d0a33ffeb3f7b7c7883fd830c54a4b1be6a4b8c7ee1f4fef0088fb - md5: e03f2c245a5ee6055752465519363b1c + size: 245246 + timestamp: 1772476886668 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + sha256: b8568dfde46edf3455458912ea6ffb760e4456db8230a0cf34ecbc557d3c275f + md5: 1ab0237036bfb14e923d6107473b0021 depends: - - krb5 >=1.21.3,<1.22.0a0 - - libsodium >=1.0.20,<1.0.21.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - libsodium >=1.0.21,<1.0.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 2527503 - timestamp: 1731585151036 -- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - sha256: 7560d21e1b021fd40b65bfb72f67945a3fcb83d78ad7ccf37b8b3165ec3b68ad - md5: df5e78d904988eb55042c0c97446079f + size: 265665 + timestamp: 1772476832995 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + sha256: 523616c0530d305d2216c2b4a8dfd3872628b60083255b89c5e0d8c42e738cca + md5: e1c36c6121a7c9c76f2f148f1e83b983 depends: - - python >=3.9 + - python >=3.10 + - python license: MIT license_family: MIT purls: - - pkg:pypi/zipp?source=hash-mapping - size: 22963 - timestamp: 1749421737203 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_1.conda - sha256: b4fd6bd1cb87a183a8bbe85b4e87a1e7c51473309d0d82cd88d38fb021bcf41e - md5: d28b82fcc8d1b462b595af4b15a6cdcf - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.11 - - libgcc >=13 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 731658 - timestamp: 1741853415477 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h536fd9c_1.conda - sha256: e884a1fc5e99904eb1c4895eb71ea7bebae35aa865422e2ff006e5b37c98d919 - md5: 22b773d9a4bcf7a25ad5bc8591abc80f - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.11 - - libgcc >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 737893 - timestamp: 1741853442447 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py312h01d7ebd_1.conda - sha256: 5d2635e81ff5d61c87383c62824988154acefeae63f408d03dbefcb80cba5f02 - md5: 493516415601e57f73bda23e91dda742 - depends: - - __osx >=10.13 - - cffi >=1.11 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 688202 - timestamp: 1741853531183 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.23.0-py313h63b0ddb_1.conda - sha256: 4b975a1ecff7947ec6fa365f01e363a0cb2521e5ef97c1561e85b7daea8581dd - md5: f00530abdc6e3dba5ae003598c8fb8a1 - depends: - - __osx >=10.13 - - cffi >=1.11 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=compressed-mapping - size: 692765 - timestamp: 1741853628130 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312hea69d52_1.conda - sha256: db7ed45ce0ed42de5b799c094f15c064e5e7e88bbee128f8d15a0565367f3c41 - md5: b0af1b749dbf9621fbea742c2de68ff8 - depends: - - __osx >=11.0 - - cffi >=1.11 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 531069 - timestamp: 1741853718145 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py313h90d716c_1.conda - sha256: 7b5035d01ee9f5e80c7a28f198d61c818891306e3b28623a8d73eeb89e17c7ad - md5: fc9329ffb94f33dd18bfbaae4d9216c6 - depends: - - __osx >=11.0 - - cffi >=1.11 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 536091 - timestamp: 1741853541598 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py312h4389bb4_1.conda - sha256: 17f2abbda821be146b549498fab3d0eb9cafb210e163b983524db91524b8dcb5 - md5: 5028543ffb67666ca4fc3ebd620c97b8 - depends: - - cffi >=1.11 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=compressed-mapping - size: 444958 - timestamp: 1741853730076 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313ha7868ed_1.conda - sha256: 711145d9cc05efe48a093db3ceecadf18f451547c94dc15745430a39ee1556d9 - md5: 0fe8f97370e74acbc7814c4906a5824f - depends: - - cffi >=1.11 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 449910 - timestamp: 1741853538921 + - pkg:pypi/zipp?source=compressed-mapping + size: 24461 + timestamp: 1776131454755 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 From 1ef41d4453ce64af248f605165225a39b8cf5860 Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 18:42:39 -0400 Subject: [PATCH 09/12] Updates for linting/tests --- pixi.lock | 2 +- pyproject.toml | 324 ++++++++++++++++++------------------ src/upstage_des/_logging.py | 2 +- 3 files changed, 164 insertions(+), 164 deletions(-) diff --git a/pixi.lock b/pixi.lock index c57eb41..534ebf8 100644 --- a/pixi.lock +++ b/pixi.lock @@ -8332,7 +8332,7 @@ packages: - pypi: ./ name: upstage-des version: 1.0.0 - sha256: dd3fcc09522a801ee403ec6cd0b42d715a8eca763c122fc43fdc9adea0d75373 + sha256: de85b703bdf671edd1af6406d681145dddf6a82d597b23f61a643b41abd876c1 requires_dist: - simpy>=4 - myst-parser ; extra == 'docs' diff --git a/pyproject.toml b/pyproject.toml index 020f5f4..8e87ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,162 +1,162 @@ -[build-system] -build-backend = "flit_core.buildapi" -requires = [ - "flit-core>=3.7.1,<4", -] - -[project] -name = "upstage-des" -version = "1.0.0" -description = "A library for behavior-driven discrete event simulation." -readme = "README.md" -keywords = [ - "agent based modeling", - "agents", - "behavior modeling", - "discrete event simulation", - "modeling", - "operations", - "simpy", - "simulation", -] -license = { file = "LICENSE" } -authors = [ - { name = "James Arruda", email = "James.Arruda@gtri.gatech.edu" }, -] -requires-python = ">=3.12" -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Education", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Scientific/Engineering", - "Topic :: Software Development :: Libraries :: Python Modules", -] -dependencies = [ - "simpy>=4", -] -optional-dependencies.docs = [ - "myst-parser", - "pydata-sphinx-theme", - "sphinx>=7", -] -optional-dependencies.lint = [ - "mypy", - "pyproject-fmt>=2.5", - "ruff>=0.6", - "ssort>=0.12", -] -optional-dependencies.test = [ - "pytest", - "pytest-cov", - "pytest-html", - "pytest-json-report", - "pytest-xdist", -] -urls."Bug Tracker" = "https://github.com/gtri/upstage/issues" -urls.Documentation = "https://gtri.github.io/upstage" -urls.Source = "https://github.com/gtri/upstage" - -[tool.flit] -sdist.exclude = [ - ".gitignore", -] - -[tool.ruff] -line-length = 100 -cache-dir = "build/.ruff_cache" -format.docstring-code-line-length = 100 -lint.select = [ - "D", - "E", - "F", - "I", - "PLE", - "PLW", - "UP", -] -lint.ignore = [ - "D105", -] -lint.per-file-ignores."tests/__*.py" = [ - "D", -] -lint.per-file-ignores."tests/test*.py" = [ - "D", -] -lint.pydocstyle.convention = "google" - -[tool.pytest] -ini_options.junit_family = "xunit2" -ini_options.cache_dir = "build/.pytest_cache" -ini_options.testpaths = [ "tests/" ] -ini_options.addopts = [ - # for contributors - "--cov-report=term-missing:skip-covered", - "--color=yes", - # for review - "--html=build/reports/pytest.html", - "--self-contained-html", - "--cov-report=html:build/reports/htmlcov", - "--cov-report=xml:build/reports/coverage.xml", - "--cov-context=test", - # coverage - "--cov=upstage_des", - "--no-cov-on-fail", - # for robots - "--junitxml=build/reports/pytest.xunit.xml", - "--json-report", - # misc - "-vv", - "--ff", -] - -[tool.coverage] -run.data_file = "build/reports/.coverage" -run.omit = [ - "*/upstage_des/utils.py", - "tests/test*.py", -] -paths.upstage_des = [ - "src/upstage_des", - "*/src/upstage_des", -] -report.exclude_also = [ - "except LookupError", - "except MotionAndDetectionError", - "except RuntimeError", - "except SimulationError", - "except TypeError", - "raise MotionAndDetectionError", - "raise NotImplementedError*", - "raise SimulationError*", - "raise TypeError", - "raise UpstageError*", -] -html.show_contexts = true - -[tool.mypy] -# broken by ipywidgetsplugins = "pydantic.mypy" -cache_dir = "build/.mypy_cache" -sqlite_cache = true -python_version = "3.12" -allow_redefinition = true -check_untyped_defs = true -disallow_untyped_defs = true -no_implicit_optional = true -show_error_codes = true -warn_return_any = true -warn_unused_ignores = true -disable_error_code = "type-abstract" -overrides = [ - { module = [ - "importlib.metadata", - ], ignore_missing_imports = true }, -] +[build-system] +build-backend = "flit_core.buildapi" +requires = [ + "flit-core>=3.7.1,<4", +] + +[project] +name = "upstage-des" +version = "1.0.0" +description = "A library for behavior-driven discrete event simulation." +readme = "README.md" +keywords = [ + "agent based modeling", + "agents", + "behavior modeling", + "discrete event simulation", + "modeling", + "operations", + "simpy", + "simulation", +] +license = { file = "LICENSE" } +authors = [ + { name = "James Arruda", email = "James.Arruda@gtri.gatech.edu" }, +] +requires-python = ">=3.12" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "simpy>=4", +] +optional-dependencies.docs = [ + "myst-parser", + "pydata-sphinx-theme", + "sphinx>=7", +] +optional-dependencies.lint = [ + "mypy", + "pyproject-fmt>=2.5", + "ruff>=0.6", + "ssort>=0.12", +] +optional-dependencies.test = [ + "pytest", + "pytest-cov", + "pytest-html", + "pytest-json-report", + "pytest-xdist", +] +urls."Bug Tracker" = "https://github.com/gtri/upstage/issues" +urls.Documentation = "https://gtri.github.io/upstage" +urls.Source = "https://github.com/gtri/upstage" + +[tool.flit] +sdist.exclude = [ + ".gitignore", +] + +[tool.ruff] +line-length = 100 +cache-dir = "build/.ruff_cache" +format.docstring-code-line-length = 100 +lint.select = [ + "D", + "E", + "F", + "I", + "PLE", + "PLW", + "UP", +] +lint.ignore = [ + "D105", +] +lint.per-file-ignores."tests/__*.py" = [ + "D", +] +lint.per-file-ignores."tests/test*.py" = [ + "D", +] +lint.pydocstyle.convention = "google" + +[tool.mypy] +# broken by ipywidgetsplugins = "pydantic.mypy" +cache_dir = "build/.mypy_cache" +sqlite_cache = true +python_version = "3.12" +allow_redefinition = true +check_untyped_defs = true +disallow_untyped_defs = true +no_implicit_optional = true +show_error_codes = true +warn_return_any = true +warn_unused_ignores = true +disable_error_code = "type-abstract" +overrides = [ + { module = [ + "importlib.metadata", + ], ignore_missing_imports = true }, +] + +[tool.pytest] +ini_options.junit_family = "xunit2" +ini_options.cache_dir = "build/.pytest_cache" +ini_options.testpaths = [ "tests/" ] +ini_options.addopts = [ + # for contributors + "--cov-report=term-missing:skip-covered", + "--color=yes", + # for review + "--html=build/reports/pytest.html", + "--self-contained-html", + "--cov-report=html:build/reports/htmlcov", + "--cov-report=xml:build/reports/coverage.xml", + "--cov-context=test", + # coverage + "--cov=upstage_des", + "--no-cov-on-fail", + # for robots + "--junitxml=build/reports/pytest.xunit.xml", + "--json-report", + # misc + "-vv", + "--ff", +] + +[tool.coverage] +run.data_file = "build/reports/.coverage" +run.omit = [ + "*/upstage_des/utils.py", + "tests/test*.py", +] +paths.upstage_des = [ + "src/upstage_des", + "*/src/upstage_des", +] +report.exclude_also = [ + "except LookupError", + "except MotionAndDetectionError", + "except RuntimeError", + "except SimulationError", + "except TypeError", + "raise MotionAndDetectionError", + "raise NotImplementedError*", + "raise SimulationError*", + "raise TypeError", + "raise UpstageError*", +] +html.show_contexts = true diff --git a/src/upstage_des/_logging.py b/src/upstage_des/_logging.py index 6c1f0fa..4a1e0b8 100644 --- a/src/upstage_des/_logging.py +++ b/src/upstage_des/_logging.py @@ -22,7 +22,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from upstage_des.actor import Actor # pragma: no cover + from upstage_des.actor import Actor # pragma: no cover ROOT_LOGGER_NAME = "upstage_des" ACTOR_LOGGER_PREFIX = f"{ROOT_LOGGER_NAME}.actor" From 6a1e7db2fcf12302c150f0fb37d74e46437cce5c Mon Sep 17 00:00:00 2001 From: James Arruda Date: Mon, 4 May 2026 18:43:31 -0400 Subject: [PATCH 10/12] Adding all version of python to lint check matrix --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0ebe9f6..4ba8267 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - environment: [py312] + environment: [py312, py313, py314] steps: - uses: actions/checkout@v6 - uses: prefix-dev/setup-pixi@v0.9.5 From e6ad0f9c9407c5dbf77a2fe6cbdd2ac67cb6c9fd Mon Sep 17 00:00:00 2001 From: James Arruda <31418520+JamesArruda@users.noreply.github.com> Date: Mon, 4 May 2026 20:49:20 -0400 Subject: [PATCH 11/12] 117 complete task testing (#124) Task test updates and new WaitUntil event. --- src/upstage_des/api.py | 2 + src/upstage_des/events.py | 96 +++++++++++++++++++++++--- src/upstage_des/tasks.py | 2 - tests/test_events.py | 23 ++++++- tests/test_knowledge.py | 5 +- tests/test_tasks.py | 137 +++++++++++++++++++++++++++++++++++++- 6 files changed, 250 insertions(+), 15 deletions(-) diff --git a/src/upstage_des/api.py b/src/upstage_des/api.py index 43ef863..cd75c80 100644 --- a/src/upstage_des/api.py +++ b/src/upstage_des/api.py @@ -32,6 +32,7 @@ Put, ResourceHold, Wait, + WaitUntil, ) from upstage_des.states import ( LinearChangingState, @@ -76,4 +77,5 @@ "Task", "TASK_GEN", "SIMPY_GEN", + "WaitUntil", ] diff --git a/src/upstage_des/events.py b/src/upstage_des/events.py index fb6bf95..5564914 100644 --- a/src/upstage_des/events.py +++ b/src/upstage_des/events.py @@ -278,25 +278,25 @@ def _convert_time(self, time: float | int, unit: str | None) -> float: def __init__( self, timeout: float | int, - timeout_unit: str | None = None, + time_unit: str | None = None, *, rehearsal_time_to_complete: float | int | None = None, ) -> None: """Create a timeout event. - If timeout_unit is specified, UPSTAGE will try to convert it to the + If time_unit is specified, UPSTAGE will try to convert it to the time_unit set in the stage. Otherwise, it defaults to that time unit. Args: timeout (float | int): Time to wait. - timeout_unit (str, optional): Units of time + time_unit (str, optional): Units of time rehearsal_time_to_complete (float | int, optional): The rehearsal time to complete. Defaults to None (the timeout given). """ if not isinstance(timeout, float | int): raise SimulationError("Bad timeout. Did you mean to use from_random_uniform?") - timeout = self._convert_time(timeout, timeout_unit) + timeout = self._convert_time(timeout, time_unit) self._time_to_complete = timeout self.timeout = timeout if self._time_to_complete < 0: @@ -310,19 +310,19 @@ def from_random_uniform( cls, low: float | int, high: float | int, - timeout_unit: str | None = None, + time_unit: str | None = None, *, rehearsal_time_to_complete: float | int | None = None, ) -> "Wait": """Create a wait from a random uniform time. - If timeout_unit is specified, UPSTAGE will try to convert it to the + If time_unit is specified, UPSTAGE will try to convert it to the time_unit set in the stage. Otherwise, it defaults to that time unit. Args: low (float): Lower bounds of random draw high (float): Upper bounds of random draw - timeout_unit (str, optional): Units of time + time_unit (str, optional): Units of time rehearsal_time_to_complete (float | int, optional): The rehearsal time to complete. Defaults to None - meaning the random value drawn. @@ -331,7 +331,7 @@ def from_random_uniform( """ rng = UpstageBase().stage.random timeout = rng.uniform(low, high) - return cls(timeout, timeout_unit, rehearsal_time_to_complete=rehearsal_time_to_complete) + return cls(timeout, time_unit, rehearsal_time_to_complete=rehearsal_time_to_complete) def as_event(self) -> SIM.Timeout: """Cast Wait event as a simpy Timeout event. @@ -356,6 +356,86 @@ def cancel(self) -> None: warn(f"Runtime error when cancelling '{self}', Error: {exc}!") +class WaitUntil(BaseEvent): + """Wait until a specific clock time. + + Rehearsal time is given by the maximum time of the interval, if given. + + Parameters + ---------- + time : int, float + Time to wait until + """ + + def _convert_time(self, time: float | int, unit: str | None) -> float: + """Convert a time to the stage time. + + Args: + time (float | int): The current time + unit (str): Units the time is in + + Returns: + float: Time in stage units + """ + base_unit = self.stage.time_unit + if base_unit is not None and unit is not None: + return unit_convert(time, unit, base_unit) + return time + + def __init__( + self, + until: float | int, + time_unit: str | None = None, + *, + rehearsal_time_to_complete: float | int | None = None, + ) -> None: + """Create a timeout event. + + If timeout_unit is specified, UPSTAGE will try to convert it to the + time_unit set in the stage. Otherwise, it defaults to that time unit. + + Args: + until (float | int): Time to wait. + time_unit (str, optional): Units of time + rehearsal_time_to_complete (float | int, optional): The rehearsal time + to complete. Defaults to None (the timeout given). + + """ + if not isinstance(until, float | int): + raise SimulationError("Bad timeout. Must ve numeric") + until_time = self._convert_time(until, time_unit) + timeout = until_time - self.env.now + self._time_to_complete = timeout + self.timeout = timeout + if self._time_to_complete < 0: + raise SimulationError(f"Negative timeout in WaitUntil: {self._time_to_complete}") + rehearse = timeout if rehearsal_time_to_complete is None else rehearsal_time_to_complete + super().__init__(rehearsal_time_to_complete=rehearse) + self._simpy_event: SIM.Timeout | None = None + + def as_event(self) -> SIM.Timeout: + """Cast WaitUntil event as a simpy Timeout event. + + Returns: + SIM.Timeout + """ + assert isinstance(self.env, SIM.Environment) + if self._simpy_event is None: + self._simpy_event = self.env.timeout(self._time_to_complete) + return self._simpy_event + + def cancel(self) -> None: + """Cancel the timeout. + + There's no real meaning to cancelling a timeout. It sits in simpy's queue either way. + """ + assert self._simpy_event is not None + try: + self._simpy_event.defused = True + except RuntimeError as exc: + warn(f"Runtime error when cancelling '{self}', Error: {exc}!") + + class All(MultiEvent): """An event that requires all events to succeed before succeeding.""" diff --git a/src/upstage_des/tasks.py b/src/upstage_des/tasks.py index a564514..93f9c15 100644 --- a/src/upstage_des/tasks.py +++ b/src/upstage_des/tasks.py @@ -10,7 +10,6 @@ from typing import Any, TypeVar from warnings import warn -from simpy import Environment as SimpyEnv from simpy import Event as SimpyEvent from simpy import Interrupt, Process @@ -251,7 +250,6 @@ def run(self, *, actor: Actor) -> Generator[SimpyEvent, None, None]: Generator[SimpyEvent, None, None]: Generator for SimPy event queue. """ self.make_decision(actor=actor) - assert isinstance(self.env, SimpyEnv) yield self.env.timeout(0.0) def run_skip(self, *, actor: "Actor") -> None: diff --git a/tests/test_events.py b/tests/test_events.py index eadaa78..24c367f 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -26,6 +26,7 @@ Put, ResourceHold, Wait, + WaitUntil, ) from upstage_des.base import SIMPY_GEN, Stage @@ -57,7 +58,7 @@ def test_wait_event() -> None: stage = Stage(time_unit="minutes") with EnvironmentContext(stage=stage) as env: - wait = Wait(timeout=1.1, timeout_unit="hours") + wait = Wait(timeout=1.1, time_unit="hours") assert wait.timeout == pytest.approx(66) with EnvironmentContext(initial_time=init_time) as env: @@ -80,6 +81,26 @@ def test_wait_event() -> None: Wait(timeout=[1, 2, 3]) # type: ignore [arg-type] +def test_wait_until_event() -> None: + init_time = 1.23 + with EnvironmentContext(initial_time=init_time) as env: + timeout = 1 + until = init_time+timeout + wait = WaitUntil(until=until) + assert wait.created_at == init_time, "Problem in environment time being stored in event" + assert wait.env is env, "Problem in environment being stored in event" + assert wait.timeout == timeout + + ret = wait.as_event() + assert isinstance(ret, SIM.Timeout), "Wait doesn't return a simpy timeout" + assert ret._delay == timeout, "Incorrect timeout time" + + stage = Stage(time_unit="minutes") + with EnvironmentContext(stage=stage) as env: + wait = WaitUntil(until=1.1, time_unit="hours") + assert wait.timeout == pytest.approx(66) + + def test_base_request_event() -> None: init_time = 1.23 with EnvironmentContext(initial_time=init_time) as env: diff --git a/tests/test_knowledge.py b/tests/test_knowledge.py index 1a1cd46..67d4c63 100644 --- a/tests/test_knowledge.py +++ b/tests/test_knowledge.py @@ -51,8 +51,9 @@ class NoKnow(Actor):... v = ma.get_knowledge("number") assert v is EMPTY_KNOWLEDGE ma.set_knowledge("number", 12.0, caller="The Test") - assert ma.get_knowledge("number", must_exist=True) == 12.0 - assert len(ma.get_log()) == 2 + assert ma.get_and_clear_knowledge("number") == 12.0 + assert len(ma.get_log()) == 3 + assert "number" not in ma.knowledge test_knowledge() diff --git a/tests/test_tasks.py b/tests/test_tasks.py index eac3784..6b5ac1b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -9,13 +9,13 @@ from typing import Any, TypedDict, cast import pytest -from simpy import Environment, Process +from simpy import Environment, Interrupt, Process from upstage_des.actor import Actor from upstage_des.base import SIMPY_GEN, EnvironmentContext, SimulationError from upstage_des.events import Wait from upstage_des.states import LinearChangingState, State -from upstage_des.tasks import InterruptStates, Task, TASK_GEN, TerminalTask +from upstage_des.tasks import DecisionTask, InterruptStates, Task, TASK_GEN, TerminalTask class Know(TypedDict): thing1: str @@ -408,3 +408,136 @@ class Dummy(Actor): task._final_interrupt = True proc.interrupt(cause="FINAL") env.run() + + +def test_markers() -> None: + class MarkedTask(Task): + def task(self, *, actor: Actor): + self.set_marker("First", InterruptStates.IGNORE) + yield Wait(1.0) + self.clear_marker() + self.set_marker("Second", InterruptStates.RESTART) + yield Wait(3.0) + self.set_marker("Third", InterruptStates.END) + yield Wait(3.0) + + with EnvironmentContext(initial_time=0.1) as env: + act = Actor(name="test") + t = MarkedTask() + proc = t.run(actor=act) + marker = t.get_marker() + assert marker is None + env.run(until=0.4) + marker = t.get_marker() + assert marker == "First" + time = t.get_marker_time() + assert time == 0.1 + assert t._interrupt_action is InterruptStates.IGNORE + # Interrupt! + proc.interrupt() + env.run(until=1.2) + assert t.get_marker() == "Second" + assert t.get_marker_time() == 1.1 + assert t._interrupt_action is InterruptStates.RESTART + # Interrupt! + proc.interrupt() + env.run(until=1.3) + assert t.get_marker() == "First" + assert t.get_marker_time() == 1.2 + env.run(until=2.3) + assert t.get_marker() == "Second" + env.run(until=5.3) + assert t.get_marker() == "Third" + + +def test_interrupt_process() -> None: + data = [] + def proc(env, t: float): + data.append("start") + try: + yield env.timeout(t) + data.append("done") + except Interrupt as e: + data.append(e.cause) + + def proc_bad(env, t: float): + data.append("start") + yield env.timeout(t) + data.append("done") + + class ProcTask(Task): + def task(self, *, actor: Actor): + _p = self.env.process(proc(self.env, 2.1)) + yield _p + + class ProcTaskBad(Task): + def task(self, *, actor: Actor): + _p = self.env.process(proc_bad(self.env, 2.1)) + yield _p + + with EnvironmentContext() as env: + act = Actor(name="example") + t = ProcTask() + p = t.run(actor=act) + env.run(until=1.3) + assert data[0] == "start" + p.interrupt(cause="CAUSE") + env.run() + assert data[-1] == "CAUSE" + + with EnvironmentContext() as env: + act = Actor(name="example") + t = ProcTaskBad() + p = t.run(actor=act) + env.run(until=1.3) + assert data[0] == "start" + p.interrupt(cause="CAUSE") + with pytest.raises(Interrupt): + env.run() + + +def test_decision_task() -> None: + class DT(DecisionTask): + def make_decision(self, *, actor: Actor): + self.set_actor_knowledge( + actor, + "exam", + "HERE", + ) + + with EnvironmentContext() as env: + act = Actor(name="Example") + dt = DT() + dt.run(actor=act) + env.run() + assert env.now == 0 + assert act.get_and_clear_knowledge("exam") == "HERE" + + +def test_task_knowledge() -> None: + class KnowTask2(Task): + def task(self, *, actor: Actor): + self.set_actor_bulk_knowledge( + actor=actor, + know={"ONE": 1, "TWO": 2.0} + ) + yield Wait(1.0) + z = self.get_and_clear_actor_knowledge(actor, "ONE") + assert z == 1 + yield Wait(1.0) + ans = self.get_and_clear_actor_bulk_knowledge(actor, names=["TWO"]) + assert ans == {"TWO": 2} + + with EnvironmentContext() as env: + act = Actor(name="Example") + kt2 = KnowTask2() + kt2.run(actor=act) + env.run(until=0.8) + assert "ONE" in act.knowledge + assert "TWO" in act.knowledge + env.run(until=1.2) + assert "ONE" not in act.knowledge + assert "TWO" in act.knowledge + env.run(until=2.2) + assert "ONE" not in act.knowledge + assert "TWO" not in act.knowledge From 97da97b6e73e576a6ed9faec197e810ea44f4e0a Mon Sep 17 00:00:00 2001 From: James Arruda <31418520+JamesArruda@users.noreply.github.com> Date: Wed, 6 May 2026 19:46:18 -0400 Subject: [PATCH 12/12] Create TaskNetworks (#126) * Typing Fixes * New knowledge type * Task networks implemented and tested * Additional Tests --- docs/source/features/logging.md | 6 +- pixi.lock | 2 +- pixi.toml | 4 +- pyproject.toml | 1 + src/upstage_des/actor.py | 359 +++++++++--- src/upstage_des/api.py | 17 +- src/upstage_des/base.py | 33 +- src/upstage_des/states.py | 18 + src/upstage_des/task_networks.py | 445 +++++++++++++++ src/upstage_des/tasks.py | 117 +++- src/upstage_des/utils/__init__.py | 1 + src/upstage_des/utils/task_net_viz.py | 158 ++++++ tests/test_active_state.py | 115 ++-- tests/test_actor_clone.py | 145 ----- tests/test_actor_state.py | 6 +- tests/test_knowledge.py | 38 +- tests/test_parallel_task_nets.py | 89 +++ tests/test_stage.py | 3 +- tests/test_task_networks.py | 787 ++++++++++++++++++++++++++ tests/test_tasks.py | 137 ++--- 20 files changed, 2107 insertions(+), 374 deletions(-) create mode 100644 src/upstage_des/task_networks.py create mode 100644 src/upstage_des/utils/__init__.py create mode 100644 src/upstage_des/utils/task_net_viz.py delete mode 100644 tests/test_actor_clone.py create mode 100644 tests/test_parallel_task_nets.py create mode 100644 tests/test_task_networks.py diff --git a/docs/source/features/logging.md b/docs/source/features/logging.md index 6fcd4d1..f06f0b1 100644 --- a/docs/source/features/logging.md +++ b/docs/source/features/logging.md @@ -29,14 +29,14 @@ arguments. Formatting is deferred — when the log level is disabled and ```{code-block} python def task(self, *, actor): - actor.log("picked up %s (qty=%d)", item, qty) # INFO - actor.log("low fuel: %.1f%%", remaining, level=logging.WARNING) + actor.write_to_log("picked up %s (qty=%d)", item, qty) # INFO + actor.write_to_log("low fuel: %.1f%%", remaining, level=logging.WARNING) ``` Two independent sinks are driven by every ``write_to_log`` call: * The per-actor in-memory list (``actor.write_to_log()`` / - ``actor.get_log()``) — controlled by the ``debug_loging`` flag set at + ``actor.get_log()``) — controlled by the ``debug_logging`` flag set at actor construction. Use this for post-run analysis in notebooks. * Python's ``logging`` — controlled by the standard level hierarchy. Use this for structured sinks (files, JSON, stdout during dev). diff --git a/pixi.lock b/pixi.lock index 534ebf8..af142a0 100644 --- a/pixi.lock +++ b/pixi.lock @@ -8332,7 +8332,7 @@ packages: - pypi: ./ name: upstage-des version: 1.0.0 - sha256: de85b703bdf671edd1af6406d681145dddf6a82d597b23f61a643b41abd876c1 + sha256: 2adcd114169bdfca75e1364e04b0f4ddccc437e0eed938fe5696a850f7856712 requires_dist: - simpy>=4 - myst-parser ; extra == 'docs' diff --git a/pixi.toml b/pixi.toml index 687c7f1..6c8cb6a 100644 --- a/pixi.toml +++ b/pixi.toml @@ -44,8 +44,8 @@ cmd = "ruff check src/ --fix" inputs = ["src/**/*.py"] [feature.tasks-lint.tasks.mypy] -cmd = "mypy --show-error-codes -p upstage_des" -inputs = ["src/**/*.py"] +cmd = "mypy --show-error-codes" +inputs = ["src/**/*.py", "tests/**/*.py"] [feature.tasks-lint.tasks.check-pyproject] cmd = "pyproject-fmt pyproject.toml --check" diff --git a/pyproject.toml b/pyproject.toml index 8e87ca4..6f6ac62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ lint.pydocstyle.convention = "google" [tool.mypy] # broken by ipywidgetsplugins = "pydantic.mypy" +files = [ "src", "tests" ] cache_dir = "build/.mypy_cache" sqlite_cache = true python_version = "3.12" diff --git a/src/upstage_des/actor.py b/src/upstage_des/actor.py index 4c9627a..3608651 100644 --- a/src/upstage_des/actor.py +++ b/src/upstage_des/actor.py @@ -8,9 +8,8 @@ import logging from collections import OrderedDict, defaultdict, deque from collections.abc import Iterable -from copy import deepcopy -from dataclasses import dataclass -from typing import Any, Self, dataclass_transform +from dataclasses import MISSING, dataclass +from typing import TYPE_CHECKING, Any, Self, dataclass_transform from simpy import Process @@ -18,11 +17,58 @@ from upstage_des.base import ( SimulationError, UpstageBase, + UpstageError, ) from upstage_des.root_types import StateDataDict from upstage_des.states import LinearChangingState, State, _ActiveState -EMPTY_KNOWLEDGE = object() +if TYPE_CHECKING: + from upstage_des.task_networks import TaskNetwork, TaskNetworkFactory + from upstage_des.tasks import Task + + +class _EMPTY_KNOWLEDGE_TYPE: + pass + + +EMPTY_KNOWLEDGE = _EMPTY_KNOWLEDGE_TYPE() + + +@dataclass +class Knowledge: + """A dataclass with dictionary-like access.""" + + def get(self, key: str, default: Any = None) -> Any: # noqa: D102 + return getattr(self, key, default) + + def keys(self) -> list[str]: + """Get keys.""" + return list(self.__dataclass_fields__.keys()) + + @classmethod + def make_blank(cls) -> Self: + """Create a blank version of ourselves.""" + inputs: dict[str, Any] = {} + for k, v in cls.__dataclass_fields__.items(): + if v.default is not MISSING: + inputs[k] = v.default + elif v.default_factory is not MISSING: + inputs[k] = v.default_factory() + else: + inputs[k] = EMPTY_KNOWLEDGE + return cls(**inputs) + + def __getitem__(self, key: str) -> Any: + return getattr(self, key) + + def __setitem__(self, key: str, value: Any) -> None: + setattr(self, key, value) + + def __contains__(self, key: str) -> bool: + return key in self.__dataclass_fields__ + + def __len__(self) -> int: + return len(self.__dataclass_fields__) @dataclass @@ -48,15 +94,18 @@ def _process_model_class(cls: type[Any]) -> None: # Use State if defined, else create one if name in base.__dict__ and isinstance(base.__dict__[name], State): field_obj = base.__dict__[name] + elif mdata := getattr(annotations[name], "__metadata__", None): + field_obj = mdata[0] + if not isinstance(field_obj, State): + raise UpstageError(f"State {name} has improper annotation. Expected a state.") else: default = base.__dict__.get(name, ...) if default is ...: field_obj = State() if name == "knowledge": - field_obj._default_factory = dict + field_obj._default_factory = annotations[name].make_blank else: field_obj = State(default=default) - # In all cases, drop the type info into the data field_obj._add_type(annotations[name]) if not getattr(field_obj, "name", None): @@ -76,10 +125,11 @@ def __init__(self: Any, **kwargs: Any) -> None: # Set up the data storage self._state_histories = {} self._log = deque() - self._is_clone = False self._state_data = {} self._states_by_cause = defaultdict(set) self._causes_by_state = {} + self._task_networks = {} + self._task_queue = {} for name in model_fields.keys(): value = kwargs.pop(name, ...) @@ -116,7 +166,6 @@ class _BaseActor(UpstageBase): Attributes: name: The actor's name debug_logging: Whether to enable debug logging - is_clone: Whether this actor is a clone (set by clone() method) Example: >>> class MyActor(BaseAct): @@ -130,9 +179,8 @@ class _BaseActor(UpstageBase): name: str debug_logging: bool - knowledge: dict[str, Any] + knowledge: Knowledge - _is_clone: bool _state_histories: dict[str, deque[tuple[float, Any] | tuple[float, Any, Any]]] _log: deque[tuple[float, str]] _state_data: dict[str, StateDataDict] @@ -140,19 +188,16 @@ class _BaseActor(UpstageBase): _states_by_cause: dict[Any, set[str]] _causes_by_state: dict[str, Any] _logger: logging.Logger + _task_networks: dict[str, "TaskNetwork"] + _task_queue: dict[str, list[str]] def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) # Apply the model transformation to every subclass _process_model_class(cls) - @property - def is_clone(self) -> bool: - """Return whether this actor is a clone.""" - return getattr(self, "_is_clone", False) - def _record_state_change(self, name: str, value: Any, extra: Any | None = None) -> None: - if name in ["name", "debug_logging", "is_clone", "knowledge"]: + if name in ["name", "debug_logging", "knowledge"]: return time = self.env.now if name not in self._state_histories: @@ -245,7 +290,7 @@ def clear_knowledge(self, name: str, caller: Any = "") -> None: """ self.write_to_log(f"Clearing {name} knowledge. Reason: {caller}") if name in self.knowledge: - del self.knowledge[name] + self.knowledge[name] = EMPTY_KNOWLEDGE def get_and_clear_knowledge(self, name: str, caller: Any = "") -> Any: """Get a knowledge value and clear it. @@ -268,7 +313,7 @@ def get_and_clear_knowledge(self, name: str, caller: Any = "") -> Any: ########################################################### ### Activate States ####################################### - def _lock_state(self, *, state: str, cause: Any) -> None: + def _lock_state(self, *, state: str, cause: "Task") -> None: """Lock one of the actor's states by a given cause. Args: @@ -285,7 +330,7 @@ def _lock_state(self, *, state: str, cause: Any) -> None: self._states_by_cause[cause].add(state) self._causes_by_state[state] = cause - def _unlock_state(self, *, state: str, cause: Any) -> None: + def _unlock_state(self, *, state: str, cause: "Task") -> None: """Unlock one of the actor's states by a given cause. If the cause didn't lock the state, an error is raised. @@ -299,83 +344,85 @@ def _unlock_state(self, *, state: str, cause: Any) -> None: self._states_by_cause[cause].remove(state) del self._causes_by_state[state] - def activate_state(self, state: str, *, cause: Any, **state_kwargs: Any) -> None: + def activate_state(self, state: str, *, task: "Task", **state_kwargs: Any) -> None: """Activate a state. Args: state (str): The name of the state to activate - cause (Any): Unique identifier or object for who activated the state. + task (Task): Unique identifier or object for who activated the state. state_kwargs (Any): Arguments to pass to the state activation. """ + self.write_to_log("%s is activating state: %s", task, state) _state = self.__model_fields__[state] assert isinstance(_state, _ActiveState) - self._lock_state(state=state, cause=cause) + self._lock_state(state=state, cause=task) _state.activate(self, **state_kwargs) - def deactivate_state(self, state: str, *, cause: Any, **kwargs: Any) -> None: + def deactivate_state(self, state: str, *, task: "Task", **kwargs: Any) -> None: """Deactivate an already active state. Args: state (str): Name of the state - cause (Any): Unique identifier or object for who activated the state. + task (Task): Unique identifier or object for who activated the state. kwargs (Any): Arguments expected by the specific state. """ + self.write_to_log("%s is deactivating state: %s", task, state) _state = self.__model_fields__[state] assert isinstance(_state, _ActiveState) - self._unlock_state(state=state, cause=cause) + self._unlock_state(state=state, cause=task) _state.deactivate(self, **kwargs) - def deactivate_states(self, states: Iterable[str], *, cause: Any, **kwargs: Any) -> None: + def deactivate_states(self, states: Iterable[str], *, task: "Task", **kwargs: Any) -> None: """Deactivate already active states. Args: states (Iterable[str]): Names of the states - cause (Any): Unique identifier or object for who activated the state. + task (Task): Unique identifier or object for who activated the state. kwargs (Any): Arguments expected by the specific state. """ for name in states: - self.deactivate_state(name, cause=cause, **kwargs) + self.deactivate_state(name, task=task, **kwargs) - def deactivate_all_states(self, *, cause: Any, **kwargs: Any) -> None: + def deactivate_all_states(self, *, task: "Task", **kwargs: Any) -> None: """Deactivate all running states. This allows no states to be deactivated if none were activated by the cause. Args: - cause (Any): Unique identifier or object for who activated the state. + task (Task): Unique identifier or object for who activated the state. kwargs (Any): Arguments expected by the states to deactivate. """ - if cause not in self._states_by_cause: + if task not in self._states_by_cause: return - state_names = list(self._states_by_cause[cause]) - self.deactivate_states(state_names, cause=cause, **kwargs) + state_names = list(self._states_by_cause[task]) + self.deactivate_states(state_names, task=task, **kwargs) - def activate_linear_state(self, state: str, rate: float, *, cause: Any) -> None: + def activate_linear_state(self, state: str, rate: float, *, task: "Task") -> None: """Activate a linear changing state. Args: state (str): The state name rate (float): The rate to change the state - cause (Any): Unique identifier or object for who is activating the state. + task (Task): Unique identifier or object for who is activating the state. """ _state = self.__model_fields__[state] assert type(_state) is LinearChangingState - self.activate_state(state, rate=rate, cause=cause) + self.activate_state(state, rate=rate, task=task) - def deactivate_linear_state(self, state: str, *, cause: Any) -> None: + def deactivate_linear_state(self, state: str, *, task: "Task") -> None: """Deactivate a linear changing state. Exists as a pair to `activate_linear_state`. Args: state (str): The state name - cause (Any): Unique identifier or object for who activated the state. + task (Task): Unique identifier or object for who activated the state. """ _state = self.__model_fields__[state] assert type(_state) is LinearChangingState - self.deactivate_state(state, cause=cause) + self.deactivate_state(state, task=task) - def make_event_for_state_goal(self, state: str, goal_value: float) -> float | None: + def get_time_for_state_goal(self, state: str, goal_value: float) -> float | None: """Get the time when a linear changing state will reach a goal value. Args: @@ -396,34 +443,218 @@ def make_event_for_state_goal(self, state: str, goal_value: float) -> float | No except Exception: return None - ########################################################### - ### Cloning ############################################### - def clone(self) -> Self: - """Create a deep copy of this actor with current state values. + ########################################################### + + ### Tasks and Networks #################################### + def add_task_network(self, network: "TaskNetwork") -> None: + """Add a task network to the actor. + + Args: + network (TaskNetwork): The task network to add to the actor. + """ + network_name = network.name + if network_name in self._task_networks: + raise SimulationError(f"Task network{network_name} already in {self}") + self._task_networks[network_name] = network + self._task_queue[network_name] = [] + + def clear_task_queue(self, network_name: str) -> None: + """Empty the actor's task queue. + + This will cause the task network to be used for task flow. + + Args: + network_name (str): The name of the network to clear the task queue. + """ + self.write_to_log(f"Clearing task queue on {network_name}") + self._task_queue[network_name] = [] - The clone: - - Has all state values deep-copied from the current actor - - Does not copy any state values that are actors - - Has no state history - - Is marked with is_clone=True - - Is not registered in the entity registry + def set_task_queue(self, network_name: str, task_list: list[str]) -> None: + """Initialize an actor's empty task queue. + + Args: + network_name (str): Task Network name + task_list (list[str]): List of task names to queue. + + Raises: + SimulationError: _description_ + """ + self.write_to_log(f"Clearing task queue on {network_name} to {task_list}") + if self._task_queue[network_name]: + raise SimulationError(f"Task queue on {self.name} is already set. Use append or clear.") + self._task_queue[network_name] = list(task_list) + + def get_task_queue(self, network_name: str) -> list[str]: + """Get the actor's task queue on a single network. + + Args: + network_name (str): The network name Returns: - Self: A cloned actor with the same state values + list[str]: List of task names in the queue """ - kwargs: dict[str, Any] = {} - for field_name, field_obj in self.__model_fields__.items(): - current_value = getattr(self, field_name) - if isinstance(current_value, Actor): - kwargs[field_name] = current_value - else: - kwargs[field_name] = deepcopy(current_value) - kwargs["name"] = kwargs["name"] + ".clone" - cloned = type(self)(**kwargs) - cloned._state_histories = {} - cloned._is_clone = True + return self._task_queue[network_name] + + def get_all_task_queues(self) -> dict[str, list[str]]: + """Get the task queues for all running networks. + + Returns: + dict[str, list[str]]: Task names, keyed on task network name. + """ + queues: dict[str, list[str]] = {} + for name in self._task_networks.keys(): + queues[name] = self.get_task_queue(name) + return queues + + def get_next_task(self, network_name: str) -> None | str: + """Return the next task the actor has been told if there is one. + + This does not clear the task, it's information only. + + Args: + network_name (str): The name of the network + + Returns: + None | str: The name of the next task, None if no next task. + """ + queue = self._task_queue[network_name] + queue_length = len(queue) + return None if queue_length == 0 else queue[0] + + def _clear_task(self, network_name: str) -> None: + """Clear a task from the queue. + + Useful for rehearsal. + """ + self._task_queue[network_name].pop(0) - return cloned + def _begin_next_task(self, network_name: str, task_name: str) -> None: + """Clear the first task in the task queue. + + The task name is required to check that the next task follows the actor's plan. + + Args: + network_name (str): The task network name + task_name (str): The name of the task to start + """ + queue = self._task_queue.get(network_name) + if queue and queue[0] != task_name: + raise SimulationError( + f"Actor {self.name} commanded to perform '{task_name}' but '{queue[0]}' is expected" + ) + elif not queue: + self.set_task_queue(network_name, [task_name]) + self.write_to_log(f"begin_next_task: Starting {task_name} task on {network_name} network.") + self._task_queue[network_name].pop(0) + + def start_network_loop( + self, + network_name: str, + init_task_name: str | None = None, + ) -> None: + """Start a task network looping/running on an actor. + + If no task name is given, it will default to following the queue. + + Args: + network_name (str): Network name. + init_task_name (str, optional): Task to start with. Defaults to None. + """ + network = self._task_networks[network_name] + network.loop(actor=self, init_task_name=init_task_name) + + def get_running_task(self, network_name: str) -> TaskData | None: + """Return name and process reference of a task on this Actor's task network. + + Useful for finding a process to call interrupt() on. + + Args: + network_name (str): Network name. + + Returns: + TaskData: Dataclass of name and process for the current task. + {"name": Name, "process": the Process simpy is holding.} + """ + if network_name not in self._task_networks: + raise SimulationError(f"{self} does not have a task networked named {network_name}") + net = self._task_networks[network_name] + if net._current_task_proc is not None: + assert net._current_task_name is not None + assert net._current_task_proc is not None + task_data = TaskData(name=net._current_task_name, process=net._current_task_proc) + return task_data + return None + + def get_running_tasks(self) -> dict[str, TaskData]: + """Get all running task data. + + Returns: + dict[str, dict[str, TaskData]]: Dictionary of all running tasks. + Keyed on network name, then {"name": Name, "process": ...} + """ + tasks: dict[str, TaskData] = {} + for name, net in self._task_networks.items(): + if net._current_task_proc is not None: + assert net._current_task_name is not None + tasks[name] = TaskData(name=net._current_task_name, process=net._current_task_proc) + return tasks + + def interrupt_network(self, network_name: str, **interrupt_kwargs: Any) -> None: + """Interrupt a running task network. + + Args: + network_name (str): The name of the network. + interrupt_kwargs (Any): kwargs to pass to the interrupt. + """ + data = self.get_running_task(network_name) + if data is None: + raise UpstageError(f"No processes named {network_name} is running.") + data.process.interrupt(**interrupt_kwargs) + + def has_task_network(self, network_id: Any) -> bool: + """Test if a network id exists. + + Args: + network_id (Any): Typically a string for the network name. + + Returns: + bool: If the task network is on this actor. + """ + return network_id in self._task_networks + + def suggest_network_name(self, factory: "TaskNetworkFactory") -> str: + """Deconflict names of task networks by suggesting a new name. + + Used for creating multiple parallel task networks. + + Args: + factory (TaskNetworkFactory): The factory from which you will create the network. + + Returns: + str: The network name to use + """ + new_name = factory.name + if new_name not in self._task_networks: + return new_name + i = 0 + while new_name in self._task_networks: + i += 1 + new_name = f"{factory.name}_{i}" + return new_name + + def delete_task_network(self, network_id: Any) -> None: + """Deletes a task network reference. + + Be careful, the network may still be running! + + Do any interruptions on your own. + + Args: + network_id (Any): Typically a string for the network name. + """ + if not self.has_task_network(network_id): + raise SimulationError(f"No networked with id: {network_id} to delete") + del self._task_networks[network_id] def _clean(self) -> None: """Run to clean all memory from the actor.""" @@ -439,7 +670,7 @@ class Actor(_BaseActor): name: str debug_logging: bool = True - knowledge: dict[str, Any] + knowledge: Knowledge = State(default_factory=Knowledge.make_blank).create() class ActorHelper: diff --git a/src/upstage_des/api.py b/src/upstage_des/api.py index cd75c80..752d1ec 100644 --- a/src/upstage_des/api.py +++ b/src/upstage_des/api.py @@ -5,13 +5,14 @@ """API for standard usage of upstage-des.""" -from upstage_des.actor import EMPTY_KNOWLEDGE, Actor +from upstage_des.actor import EMPTY_KNOWLEDGE, Actor, Knowledge from upstage_des.base import ( ENTITY_REGISTRY_CONTEXT_VAR, ENV_CONTEXT_VAR, SIMPY_GEN, STAGE_CONTEXT_VAR, EnvironmentContext, + SimulationEnd, SimulationError, Stage, UpstageBase, @@ -38,15 +39,23 @@ LinearChangingState, State, ) +from upstage_des.task_networks import ( + TaskLinks, + TaskNetwork, + TaskNetworkFactory, + TaskTransition, +) from upstage_des.tasks import ( TASK_GEN, DecisionTask, InterruptStates, Task, + TerminalTask, ) __all__ = [ "Actor", + "Knowledge", "EMPTY_KNOWLEDGE", "ENTITY_REGISTRY_CONTEXT_VAR", "ENV_CONTEXT_VAR", @@ -78,4 +87,10 @@ "TASK_GEN", "SIMPY_GEN", "WaitUntil", + "TaskLinks", + "TaskNetwork", + "TaskNetworkFactory", + "TaskTransition", + "TerminalTask", + "SimulationEnd", ] diff --git a/src/upstage_des/base.py b/src/upstage_des/base.py index e3f2b8d..e1468df 100644 --- a/src/upstage_des/base.py +++ b/src/upstage_des/base.py @@ -27,6 +27,10 @@ class UpstageError(Exception): """Raised when an UPSTAGE error happens or expectation is not met.""" +class SimulationEnd(Exception): + """Raised when you want to end the sim, but know it was a safe end.""" + + @dataclass class Stage: """Simulation stage configuration and shared state. @@ -95,7 +99,6 @@ def __init__(self, message: str, time: float | None = None): ENV_CONTEXT_VAR: ContextVar[SimpyEnv] = ContextVar("Environment") STAGE_CONTEXT_VAR: ContextVar[Stage] = ContextVar("Stage") ENTITY_REGISTRY_CONTEXT_VAR: ContextVar[dict[str, list[Any]]] = ContextVar("EntityRegistry") -REHEARSAL_CONTEXT_VAR: ContextVar[bool] = ContextVar("Rehearsing") class UpstageBase: @@ -229,7 +232,6 @@ def __init__( self.env_ctx = ENV_CONTEXT_VAR self.stage_ctx = STAGE_CONTEXT_VAR self.entity_registry_ctx = ENTITY_REGISTRY_CONTEXT_VAR - self.rehearsal_ctx = REHEARSAL_CONTEXT_VAR self.env_token: Token[SimpyEnv] self.stage_token: Token[Stage] self.entity_registry_token: Token[dict[str, list[Any]]] @@ -269,8 +271,6 @@ def __enter__(self) -> SimpyEnv: entity_registry: dict[str, list[Any]] = {} self.entity_registry_token = self.entity_registry_ctx.set(entity_registry) - self.rehearsal_token = self.rehearsal_ctx.set(False) - return self._env def __exit__(self, *_: Any) -> None: @@ -278,7 +278,6 @@ def __exit__(self, *_: Any) -> None: self.env_ctx.reset(self.env_token) self.stage_ctx.reset(self.stage_token) self.entity_registry_ctx.reset(self.entity_registry_token) - self.rehearsal_ctx.reset(self.rehearsal_token) self._env = None @@ -394,23 +393,12 @@ def get_entities_by_class(class_name: str) -> list[Any]: return registry.get(class_name, []) -def set_rehearsing(value: bool) -> None: - """Set the rehearsal context var. - - When True, it prevents @process from going to simpy. - - Args: - value (bool): The rehearsal value to set. - """ - REHEARSAL_CONTEXT_VAR.set(value) - - PROC = Generator[SimpyEvent, Any, None] def process( func: Callable[..., PROC], -) -> Callable[..., Process | PROC]: +) -> Callable[..., Process]: """Decorate a ``simpy`` process to schedule it as a callable. Allows users to decorate a generator, and when they want to schedule them @@ -449,20 +437,13 @@ def process( """ @wraps(func) - def wrapped_generator(*args: Any, **kwargs: Any) -> Process | PROC: + def wrapped_generator(*args: Any, **kwargs: Any) -> Process: """Wrap the generator with a function that calls it as a process.""" try: environment = ENV_CONTEXT_VAR.get() except LookupError: raise SimulationError("No environment found on process call") - try: - rehearsing = REHEARSAL_CONTEXT_VAR.get() - except LookupError: - rehearsing = False f = func(*args, **kwargs) - if not rehearsing: - return environment.process(f) - else: - return f + return environment.process(f) return wrapped_generator diff --git a/src/upstage_des/states.py b/src/upstage_des/states.py index a2f7471..07e97b4 100644 --- a/src/upstage_des/states.py +++ b/src/upstage_des/states.py @@ -165,6 +165,24 @@ def __set_name__(self, owner: type, name: str) -> None: """ self.name = name + def create(self) -> T: + """Create the state in a way that mypy likes. + + This is optional, but if you are using `mypy` to check your simulation + build, then: + + class Mover(Actor): + distance_traveled: float = State(default=0.0) + + will fail. Using + + class Mover(Actor): + distance_traveled: float = State(default=0.0).create() + + will make mypy happy. + """ + return self # type: ignore[return-value] + def _add_type(self, typing: Any) -> None: self._given_type = typing diff --git a/src/upstage_des/task_networks.py b/src/upstage_des/task_networks.py new file mode 100644 index 0000000..539485a --- /dev/null +++ b/src/upstage_des/task_networks.py @@ -0,0 +1,445 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""The task network class, and factory classes.""" + +from collections.abc import Callable, Generator, Mapping, Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING +from warnings import warn + +if TYPE_CHECKING: + from upstage_des.actor import Actor + +from simpy import Process + +from upstage_des.base import SimulationError, UpstageError, process +from upstage_des.tasks import DecisionTask, Task, TerminalTask + +GUARD_FUNC = Callable[..., bool] + + +@dataclass +class TaskTransition: + """A resolved transition in a task network. + + Users typically pass raw tuples to :class:`TaskLinks`; those are + normalised to :class:`Transition` when the network is constructed. + """ + + target: str | type[Task] + guard: GUARD_FUNC | None + label: str | None = None + + +@dataclass +class TaskLinks: + """Describes the transitions from one task to others in a network. + + There are two styles for defining transitions: + + **Preference style** (``default`` + ``allowed``):: + + TaskLinks(default="B", allowed=["B", "C"]) + + **Guard style** (``transitions``) — an ordered list of + ``(target, guard_or_None)`` or ``(target, guard, label)`` tuples. + The first guard that returns ``True`` (or is ``None``, meaning + unconditional) wins. An optional third element provides a + human-readable label for diagrams:: + + TaskLinks(transitions=[ + TaskTransition(Break, lambda actor: actor.needs_break, "needs break"), + TaskTransition(DoCheckout, None), # fallback + ]) + + Both ``default``/``allowed`` and ``transitions`` accept task class + references or string names. Class references are resolved to their + ``__name__`` when the network is constructed. + """ + + default: str | type[Task] | None = None + allowed: Sequence[str | type[Task]] = field(default_factory=list) + transitions: Sequence[TaskTransition] = field(default_factory=list) + + def _resolve(self) -> "TaskLinks": + """Return a copy with all class references replaced by ``__name__`` strings.""" + default = self.default.__name__ if isinstance(self.default, type) else self.default + allowed = [a.__name__ if isinstance(a, type) else a for a in self.allowed] + resolved_trans: list[TaskTransition] = [] + for tr in self.transitions: + target = tr.target.__name__ if isinstance(tr.target, type) else tr.target + label = tr.label + resolved_trans.append(TaskTransition(target=target, guard=tr.guard, label=label)) + return TaskLinks(default=default, allowed=allowed, transitions=resolved_trans) + + def _all_targets(self) -> list[str | type[Task]]: + """Return every target referenced by this link (for validation/viz).""" + targets: list[str | type[Task]] = list(self.allowed) + if self.default is not None and self.default not in targets: + targets.append(self.default) + for tr in self.transitions: + if tr.target not in targets: + targets.append(tr.target) + return targets + + +def _validate_network( + task_classes: Mapping[str, type[Task]], + task_links: Mapping[str, TaskLinks], +) -> None: + """Check that all task-link references point to tasks that exist. + + Args: + task_classes: Task name to class mapping. + task_links: Task name to link mapping. + + Raises: + UpstageError: If a referenced task name is not in *task_classes*. + """ + known = set(task_classes.keys()) + missing: set[str] = set() + for src, links in task_links.items(): + for target in links._all_targets(): + if isinstance(target, str) and target and target not in known: + missing.add(target) + if missing: + raise UpstageError( + f"Task link(s) reference unknown task name(s): {sorted(missing)}. " + f"Known tasks: {sorted(known)}" + ) + unlinked = known - set(task_links.keys()) + if unlinked: + warn( + f"Task(s) {sorted(unlinked)} are in task_classes but have no entry in task_links.", + UserWarning, + stacklevel=3, + ) + + +class TaskNetwork: + """A means to represent, execute, and rehearse interdependent tasks.""" + + def __init__( + self, + name: str, + task_classes: Mapping[str, type[Task]], + task_links: Mapping[str, TaskLinks], + ) -> None: + """Create a task network. + + Task links are defined as: + {task_name: TaskLinks(default= task_name | None, allowed= list[task_names]} + where each task has a default next task (or None), and tasks that could follow it. + + Args: + name (str): Network name + task_classes (Mapping[str, Task]): Task names to Task object mapping. + task_links (Mapping[str, TaskLinks]): Task links. + """ + self.name = name + self.task_classes = task_classes + self.task_links = task_links + self._current_task_name: str | None = None + self._current_task_inst: Task | None = None + self._current_task_proc: Process | None = None + _validate_network(task_classes, task_links) + + def _next_task_name( + self, curr_task_name: str, actor: "Actor", clear_queue: bool = False + ) -> str: + """Get the next task name. + + Priority: + 1. Actor's task queue (imperative override from interrupts etc.) + 2. Guard-based transitions (first ``True`` guard wins) + 3. ``default`` fallback + + Returns: + str: Task name + """ + # 1. Check the queue first (imperative override) + task_from_queue = actor.get_next_task(self.name) + if task_from_queue is not None: + if clear_queue: + actor._clear_task(self.name) + return task_from_queue + + links = self.task_links[curr_task_name] + + # 2. Evaluate guards + for tr in links.transitions: + if tr.guard is None or tr.guard(actor): + return tr.target if isinstance(tr.target, str) else tr.target.__name__ + + # 3. Fall back to default + default_next_task = links.default + if default_next_task is None: + raise SimulationError( + f"No default task set for after {curr_task_name} on {actor} and no guard matched." + ) + assert isinstance(default_next_task, str) + return default_next_task + + @process + def loop( + self, *, actor: "Actor", init_task_name: str | None = None + ) -> Generator[Process, None, None]: + """Start a task network running its loop. + + If no initial task name is given, it will default to following the queue. + + Args: + actor (Actor): The actor to run the loop on. + init_task_name (Optional[str], optional): Optional task to start running. + Defaults to None. + """ + next_name = actor.get_next_task(self.name) + if next_name is None: + if init_task_name is None: + raise SimulationError( + f"Actor {actor} wasn't supplied an initial task" + ) # pramga: no cover + next_name = init_task_name + + self._current_task_name = next_name + + while True: + task_name = self._current_task_name + assert isinstance(task_name, str) + actor.write_to_log("Outer: starting %s", task_name) + actor._begin_next_task(self.name, task_name) + task_cls = self.task_classes[task_name] + task_instance: Task = task_cls() + self._current_task_inst = task_instance + self._current_task_inst._set_network_name(self.name) + self._current_task_inst._set_network_ref(self) + + task_instance.on_enter(actor=actor) + + if ( + isinstance(self._current_task_inst, DecisionTask) + and self._current_task_inst.DO_NOT_HOLD + ): + self._current_task_inst.run_skip(actor=actor) + else: + self._current_task_proc = self._current_task_inst.run(actor=actor) + yield self._current_task_proc + + task_instance.on_exit(actor=actor) + + next_name = self._next_task_name(task_name, actor) + self._current_task_name = next_name + + def __repr__(self) -> str: + return f"Task network: {self.name}" + + +class TaskNetworkFactory: + """A factory for creating task network instances. + + The constructor accepts two styles: + + **String-keyed (original API)**:: + + TaskNetworkFactory("Net", {"A": ATask, "B": BTask}, + {"A": TaskLinks("B", ["B"]), ...}) + + **Class-keyed (new API)** — ``task_classes`` is derived automatically:: + + TaskNetworkFactory("Net", task_links={ + ATask: TaskLinks(default=BTask, allowed=[BTask]), + ... + }) + """ + + @staticmethod + def _resolve_inputs( + task_classes: Mapping[str, type[Task]] | Mapping[type[Task], TaskLinks] | None, + task_links: Mapping[str, TaskLinks] | Mapping[type[Task], TaskLinks] | None, + ) -> tuple[dict[str, type[Task]], dict[str, TaskLinks]]: + """Normalise the two constructor styles into ``(str→class, str→TaskLinks)``.""" + if task_classes is None and task_links is None: + raise UpstageError("At least one of task_classes or task_links must be provided.") + + # Detect the class-keyed style: keys are types, not strings + class_keyed_map: Mapping[type[Task], TaskLinks] | None = None + + if task_links is not None and task_classes is None: + # task_links only — must be class-keyed + if all(isinstance(k, type) for k in task_links): + class_keyed_map = task_links # type: ignore[assignment] + else: + raise UpstageError( + "When task_classes is omitted, task_links keys must be Task classes." + ) + elif task_links is None and task_classes is not None: + # task_classes only — check if it's actually the class-keyed form + if all(isinstance(k, type) for k in task_classes): + class_keyed_map = task_classes # type: ignore[assignment] + else: + raise UpstageError( + "When task_links is omitted, task_classes must be a " + "{TaskClass: TaskLinks} mapping." + ) + elif task_links is not None and task_classes is not None: + # Both provided — check if task_links uses class keys + if all(isinstance(k, type) for k in task_links): + class_keyed_map = task_links # type: ignore[assignment] + elif all(isinstance(k, str) for k in task_classes) and all( + isinstance(k, str) for k in task_links + ): + # Traditional string-keyed API — resolve any class refs in TaskLinks values + str_links: dict[str, TaskLinks] = {} + for k, v in task_links.items(): + assert isinstance(k, str) + str_links[k] = v._resolve() + str_classes = {k: v for k, v in task_classes.items() if isinstance(k, str)} + return str_classes, str_links # type: ignore[return-value] + else: + raise UpstageError( + "Cannot mix class and string keys across task_classes/task_links." + ) + + # ---- resolve class-keyed form ---- + assert class_keyed_map is not None + out_classes: dict[str, type[Task]] = {} + out_links: dict[str, TaskLinks] = {} + for cls, links in class_keyed_map.items(): + task_name = cls.__name__ + out_classes[task_name] = cls + resolved = links._resolve() + out_links[task_name] = resolved + # Also collect classes referenced in defaults / allowed / transitions + for target in links._all_targets(): + if isinstance(target, type): + out_classes.setdefault(target.__name__, target) + + return out_classes, out_links + + def __init__( + self, + name: str, + task_classes: Mapping[str, type[Task]] | Mapping[type[Task], TaskLinks] | None = None, + task_links: Mapping[str, TaskLinks] | Mapping[type[Task], TaskLinks] | None = None, + ) -> None: + """Create a factory for making instances of a task network. + + Args: + name (str): The network name + task_classes: ``{str: Task}`` mapping **or** ``{Task: TaskLinks}`` + mapping (class-keyed style). May be *None* when *task_links* + uses class keys. + task_links: ``{str: TaskLinks}`` or ``{Task: TaskLinks}`` mapping. + May be *None* when *task_classes* carries the class-keyed form. + """ + self.name = name + resolved_classes, resolved_links = self._resolve_inputs(task_classes, task_links) + self.task_classes: Mapping[str, type[Task]] = resolved_classes + self.task_links: Mapping[str, TaskLinks] = resolved_links + _validate_network(self.task_classes, self.task_links) + + @classmethod + def from_single_looping(cls, name: str, task_class: type[Task]) -> "TaskNetworkFactory": + """Create a network factory from a single task that loops. + + Args: + name (str): Network name + task_class (Task): The single task to loop + + Returns: + TaskNetworkFactory: The factory for the single looping network. + """ + taskname = task_class.__name__ + task_classes = {taskname: task_class} + task_links: dict[str, TaskLinks] = { + taskname: TaskLinks(default=taskname, allowed=[taskname]) + } + return TaskNetworkFactory(name, task_classes, task_links) + + @classmethod + def from_single_terminating(cls, name: str, task_class: type[Task]) -> "TaskNetworkFactory": + """Create a network factory from a single task that terminates. + + Args: + name (str): Network name + task_class (Task): The single task to terminate after + + Returns: + TaskNetworkFactory: The factory for the single terminating network. + """ + taskname = task_class.__name__ + end_name = f"{taskname}_FINAL" + task_classes = {taskname: task_class, end_name: TerminalTask} + task_links: dict[str, TaskLinks] = { + taskname: TaskLinks(default=end_name, allowed=[end_name]) + } + return TaskNetworkFactory(name, task_classes, task_links) + + @classmethod + def from_ordered_terminating( + cls, name: str, task_classes: list[type[Task]] + ) -> "TaskNetworkFactory": + """Create a network factory from a list of tasks that terminates. + + Args: + name (str): Network name + task_classes (list[Task]): The tasks to run in order. + + Returns: + TaskNetworkFactory: The factory for the ordered network. + """ + task_class = {} + task_links: dict[str, TaskLinks] = {} + for i, tc in enumerate(task_classes): + the_name = tc.__name__ + task_class[the_name] = tc + try: + nxt = task_classes[i + 1] + nxt_name = nxt.__name__ + except IndexError: + nxt = TerminalTask + nxt_name = f"{name}_TERMINATING" + task_class[nxt_name] = nxt + task_links[the_name] = TaskLinks(default=nxt_name, allowed=[nxt_name]) + return TaskNetworkFactory(name, task_class, task_links) + + @classmethod + def from_ordered_loop(cls, name: str, task_classes: list[type[Task]]) -> "TaskNetworkFactory": + """Create a network factory from a list of tasks that loops. + + Args: + name (str): Network name + task_classes (list[Task]): The tasks to run in order. + + Returns: + TaskNetworkFactory: The factory for the ordered network. + """ + task_class = {} + task_links: dict[str, TaskLinks] = {} + for i, tc in enumerate(task_classes): + the_name = tc.__name__ + task_class[the_name] = tc + try: + nxt = task_classes[i + 1] + except IndexError: + nxt = task_classes[0] + nxt_name = nxt.__name__ + task_links[the_name] = TaskLinks(default=nxt_name, allowed=[nxt_name]) + return TaskNetworkFactory(name, task_class, task_links) + + def make_network(self, other_name: str | None = None) -> TaskNetwork: + """Create an instance of the task network. + + By default, this uses the name defined on instantiation. + + Args: + other_name (str, optional): Another name for the network. Defaults to None. + + Returns: + TaskNetwork + """ + use_name = other_name if other_name is not None else self.name + return TaskNetwork(use_name, self.task_classes, self.task_links) diff --git a/src/upstage_des/tasks.py b/src/upstage_des/tasks.py index 93f9c15..b90f0dc 100644 --- a/src/upstage_des/tasks.py +++ b/src/upstage_des/tasks.py @@ -7,16 +7,20 @@ from collections.abc import Generator from enum import IntFlag -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from warnings import warn from simpy import Event as SimpyEvent from simpy import Interrupt, Process -from upstage_des.actor import Actor, ActorHelper +from upstage_des.actor import ActorHelper from upstage_des.base import SimulationError, UpstageBase, process from upstage_des.events import BaseEvent, Event +if TYPE_CHECKING: + from upstage_des.actor import Actor + from upstage_des.task_networks import TaskNetwork + __all__ = ("DecisionTask", "Task", "process", "TerminalTask", "InterruptStates") @@ -50,12 +54,35 @@ def __init__(self) -> None: self._marked_time: float | None = None self._interrupt_action: InterruptStates = InterruptStates.END self._final_interrupt: bool = False + self._network_ref: TaskNetwork | None = None + self._network_name: str | None = None - def task(self, *, actor: Actor) -> TASK_GEN: + def task(self, *, actor: Any) -> TASK_GEN: """Define the process this task follows.""" raise NotImplementedError(NOT_IMPLEMENTED_MSG) - def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: + def on_enter(self, *, actor: Any) -> None: + """Zero-time hook called before ``task()`` runs. + + Use this for setup that would otherwise require a ``DecisionTask``: + setting knowledge, acquiring resources, initializing state. + + Args: + actor: The actor about to execute this task. + """ + ... + + def on_exit(self, *, actor: Any) -> None: + """Zero-time hook called after ``task()`` completes (before guards). + + Use this for cleanup: clearing knowledge, recording results. + + Args: + actor: The actor that just finished this task. + """ + ... + + def on_interrupt(self, *, actor: Any, cause: Any) -> InterruptStates: """Define any actions to take on the actor if this task is interrupted. Note: @@ -108,8 +135,72 @@ def clear_marker(self) -> None: self._marked_time = None self._interrupt_action = InterruptStates.END + def _set_network_ref(self, network: "TaskNetwork") -> None: + """Set the reference to the task network object. + + Args: + network (TaskNetwork): The network + """ + if self._network_ref is not None: + raise SimulationError( + "Setting task network reference on task that already has a network" + ) + self._network_ref = network + + def _set_network_name(self, network_name: str) -> None: + """Set the name of the network this task is in. + + Args: + network_name (str): Network name + """ + if self._network_name is not None: + raise SimulationError("Setting task network name on task that already has a network") + self._network_name = network_name + + def clear_actor_task_queue(self, actor: "Actor") -> None: + """Clear out the task queue on the network. + + Args: + actor (Actor): The actor whose queue will be cleared + """ + assert self._network_name is not None + actor.clear_task_queue(self._network_name) + + def set_actor_task_queue(self, actor: "Actor", task_list: list[str]) -> None: + """Set the task queue on the actor. + + This assumes an empty queue. + + Args: + actor (Actor): The actor to modify the task queue of + task_list (list[str]): The list of task names to queue. + """ + assert self._network_name is not None + actor.set_task_queue(self._network_name, task_list) + + def get_actor_task_queue(self, actor: "Actor") -> list[str]: + """Get the task queue on the actor. + + Args: + actor (Actor): The actor to modify the task queue of + """ + assert self._network_name is not None + return actor.get_task_queue(self._network_name) + + def get_actor_next_task(self, actor: "Actor") -> str | None: + """Get the next queued task. + + Args: + actor (Actor): The actor to get the next task from + + Returns: + str | None: The next task name (or None if no task) + """ + assert self._network_name is not None + return actor.get_next_task(self._network_name) + def _handle_interruption( - self, actor: Actor, interrupt: Interrupt, next_event: BaseEvent | Process + self, actor: "Actor", interrupt: Interrupt, next_event: BaseEvent | Process ) -> InterruptStates: """Clean up after an interrupt and perform interrupt checks/actions. @@ -134,7 +225,7 @@ def _handle_interruption( or self._final_interrupt ): actor.write_to_log(f"Interrupted by {interrupt}.") - actor.deactivate_all_states(cause=self) + actor.deactivate_all_states(task=self) if isinstance(next_event, BaseEvent): names = list(actor.knowledge.keys()) for name in names: @@ -152,7 +243,7 @@ def _handle_interruption( return _interrupt_action @process - def run(self, *, actor: Actor) -> Generator[SimpyEvent | Process, Any, None]: + def run(self, *, actor: "Actor") -> Generator[SimpyEvent | Process, Any, None]: """Execute the task. Args: @@ -231,16 +322,16 @@ class DecisionTask(Task): DO_NOT_HOLD: bool = False - def task(self, *, actor: Actor) -> TASK_GEN: + def task(self, *, actor: Any) -> TASK_GEN: """Define the process this task follows.""" raise SimulationError("No need to call `task` on a DecisionTask") - def make_decision(self, *, actor: Actor) -> None: + def make_decision(self, *, actor: Any) -> None: """Define the process this task follows.""" raise NotImplementedError(NOT_IMPLEMENTED_MSG) @process - def run(self, *, actor: Actor) -> Generator[SimpyEvent, None, None]: + def run(self, *, actor: "Actor") -> Generator[SimpyEvent, None, None]: """Run the decision task. Args: @@ -274,7 +365,7 @@ class TerminalTask(Task): _time_to_complete: float = 1e24 - def log_message(self, *, actor: Actor) -> str: + def log_message(self, *, actor: "Actor") -> str: """A message to save to a log when this task is reached. Args: @@ -285,7 +376,7 @@ def log_message(self, *, actor: Actor) -> str: """ return f"Entering terminal task: {self}" - def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: + def on_interrupt(self, *, actor: "Actor", cause: Any) -> InterruptStates: """Special case interrupt for terminal task. Args: @@ -298,7 +389,7 @@ def on_interrupt(self, *, actor: Actor, cause: Any) -> InterruptStates: ) return InterruptStates.END - def task(self, *, actor: Actor) -> TASK_GEN: + def task(self, *, actor: "Actor") -> TASK_GEN: """The terminal task. It's just a long wait. diff --git a/src/upstage_des/utils/__init__.py b/src/upstage_des/utils/__init__.py new file mode 100644 index 0000000..e126d31 --- /dev/null +++ b/src/upstage_des/utils/__init__.py @@ -0,0 +1 @@ +"""Various utilities for UPSTAGE features.""" diff --git a/src/upstage_des/utils/task_net_viz.py b/src/upstage_des/utils/task_net_viz.py new file mode 100644 index 0000000..46301b3 --- /dev/null +++ b/src/upstage_des/utils/task_net_viz.py @@ -0,0 +1,158 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Visualization builders for task networks.""" + +from upstage_des.task_networks import GUARD_FUNC, TaskNetwork, TaskNetworkFactory + + +def _guard_label(guard: GUARD_FUNC | None) -> str: + """Extract a human-readable label from a guard function.""" + if guard is None: + return "" + name: str = getattr(guard, "__name__", "") + if name and name != "": + return name + qualname: str = getattr(guard, "__qualname__", "") + if qualname and "" not in qualname: + return qualname + return "guard" + + +def _hook_suffix(net: TaskNetwork, task_name: str) -> str: + """Return a parenthesized hook list, or empty string.""" + cls = net.task_classes.get(task_name) + if cls is None: + return "" + hooks: list[str] = [] + if "on_enter" in cls.__dict__: + hooks.append("on_enter") + if "on_exit" in cls.__dict__: + hooks.append("on_exit") + if not hooks: + return "" + return f"({', '.join(hooks)})" + + +def to_mermaid(net: TaskNetwork | TaskNetworkFactory, *, legend: bool = True) -> str: + """Return a Mermaid graph diagram of the task network. + + Renders in Jupyter, GitHub Markdown, and any Mermaid-compatible viewer. + Tasks with ``on_enter`` or ``on_exit`` hooks are annotated. + + Args: + net (TaskNetwork | TaskNetworkFactory): The network to visualize + legend: Show a legend distinguishing solid (transition) and + dashed (allowed/queue) edges. Defaults to True; only + rendered when dashed edges are present. + + Returns: + str: Mermaid diagram source. + """ + if isinstance(net, TaskNetworkFactory): + net = net.make_network() + has_allowed = False + lines = ["graph TD"] + # Declare every task node so rendering is consistent whether or not + # a task defines on_enter/on_exit hooks. + for task_name in net.task_classes: + node_id = task_name.replace(" ", "_") + suffix = _hook_suffix(net, task_name) + if suffix: + lines.append(f' {node_id}["{task_name}
{suffix}"]') + else: + lines.append(f' {node_id}["{task_name}"]') + # Edges + for src, links in net.task_links.items(): + src_id = src.replace(" ", "_") + transitions = links.transitions + trans_targets = {tr.target for tr in transitions} + for tr in transitions: + label = tr.label if tr.label is not None else _guard_label(tr.guard) + tgt_id = tr.target.__name__ if isinstance(tr.target, type) else tr.target + tgt_id = tgt_id.replace(" ", "_") + if label: + lines.append(f" {src_id} -->|{label}| {tgt_id}") + else: + lines.append(f" {src_id} --> {tgt_id}") + if links.default is not None: + assert isinstance(links.default, str) + def_id = links.default.replace(" ", "_") + if links.default not in trans_targets: + lines.append(f" {src_id} --> {def_id}") + for a in links.allowed: + assert isinstance(a, str) + a_id = a.replace(" ", "_") + if a not in trans_targets and a != links.default: + has_allowed = True + lines.append(f" {src_id} -.-> {a_id}") + # Legend + if legend and has_allowed: + lines.append("") + lines.append(" subgraph Legend[ ]") + lines.append(" direction LR") + lines.append(" L1[ ] -->|transition| L2[ ]") + lines.append(" L3[ ] -.->|via queue| L4[ ]") + lines.append(" end") + lines.append(" style Legend fill:none,stroke:#ccc") + lines.append(" style L1 fill:none,stroke:none,width:0px") + lines.append(" style L2 fill:none,stroke:none,width:0px") + lines.append(" style L3 fill:none,stroke:none,width:0px") + lines.append(" style L4 fill:none,stroke:none,width:0px") + return "\n".join(lines) + + +def to_dot(net: TaskNetwork | TaskNetworkFactory) -> str: + """Return a Graphviz DOT diagram of the task network. + + Tasks with ``on_enter`` or ``on_exit`` hooks are annotated. + + Args: + net (TaskNetwork | TaskNetworkFactory): The network to visualize + + Returns: + str: DOT source string. + """ + if isinstance(net, TaskNetworkFactory): + net = net.make_network() + lines = [ + f"digraph {net.name.replace(' ', '_')} {{", + " rankdir=TB;", + ' node [shape=box, style=rounded, fontname="Helvetica"];', + ' edge [fontname="Helvetica", fontsize=10];', + "", + ] + # Declare every task node so rendering is consistent whether or not + # a task defines on_enter/on_exit hooks. + for task_name in net.task_classes: + suffix = _hook_suffix(net, task_name) + if suffix: + lines.append( + f' "{task_name}" [label=<{task_name}
' + f'{suffix}>];' + ) + else: + lines.append(f' "{task_name}";') + lines.append("") + # Edges + for src, links in net.task_links.items(): + transitions = links.transitions + trans_targets = {tr.target for tr in transitions} + for tr in transitions: + label = tr.label if tr.label is not None else _guard_label(tr.guard) + if label: + lines.append(f' "{src}" -> "{tr.target}" [label="{label}"];') + else: + lines.append(f' "{src}" -> "{tr.target}";') + if links.default is not None: + assert isinstance(links.default, str) + if links.default not in trans_targets: + lines.append(f' "{src}" -> "{links.default}";') + for a in links.allowed: + assert isinstance(a, str) + if a not in trans_targets and a != links.default: + lines.append(f' "{src}" -> "{a}" [style=dashed];') + lines.append("}") + return "\n".join(lines) diff --git a/tests/test_active_state.py b/tests/test_active_state.py index 0e0ce78..e57d2f3 100644 --- a/tests/test_active_state.py +++ b/tests/test_active_state.py @@ -6,19 +6,22 @@ """Test the active state""" from collections import deque +from typing import Annotated import pytest from upstage_des.actor import Actor from upstage_des.states import LinearChangingState from upstage_des.base import EnvironmentContext, SimulationError - +from upstage_des.tasks import Task def test_linear_state() -> None: class MyActor(Actor): rate_state: float - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor( name="Rated", rate_state=2.0, @@ -27,7 +30,7 @@ class MyActor(Actor): env.run(1.0) act.level=1. env.run(1.5) - act.activate_linear_state("level", act.rate_state, cause="TEST") + act.activate_linear_state("level", act.rate_state, task=T) env.run(2.5) assert act.level == 3.0 assert act._state_histories["level"] == deque([ @@ -37,7 +40,7 @@ class MyActor(Actor): (2.5, 3.0), ]) env.run(3.0) - act.deactivate_state("level", cause="TEST") + act.deactivate_state("level", task=T) env.run(4.0) assert act.level == 4.0 env.run(5.0) @@ -47,12 +50,16 @@ class MyActor(Actor): assert len(act._state_histories["level"]) == 6 +test_linear_state() + def test_linear_predict() -> None: class MyActor(Actor): rate_state: float - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor( name="Rated", rate_state=2.0, @@ -62,43 +69,47 @@ class MyActor(Actor): assert isinstance(field, LinearChangingState) assert field.predict_value_time(act, 6.4) is None env.run(0.5) - act.activate_linear_state("level", rate=3.2, cause="TEST") + act.activate_linear_state("level", rate=3.2, task=T) assert field.predict_value_time(act, 6.4) == 2.5 env.run(1.5) assert field.predict_value_time(act, 6.4) == 2.5 env.run(3.5) assert field.predict_value_time(act, 6.4) is None - act.deactivate_linear_state("level", cause="TEST") - act.activate_linear_state("level", rate=-3.0, cause="TEST") + act.deactivate_linear_state("level", task=T) + act.activate_linear_state("level", rate=-3.0, task=T) assert field.predict_value_time(act, 6.4) is not None def test_linear_errors() -> None: class MyActor(Actor): rate_state: float - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor( name="Rated", rate_state=2.0, level=0.0, ) with pytest.raises(SimulationError): - act.deactivate_linear_state("level", cause="TEST") - act.activate_linear_state("level", 1.3, cause="TEST") + act.deactivate_linear_state("level", task=T) + act.activate_linear_state("level", 1.3, task=T) with pytest.raises(SimulationError): - act.activate_state("level", rate=3.4, cause="TEST") + act.activate_state("level", rate=3.4, task=T) def test_linear_negative_rate() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Drainer", level=100.0) env.run(1.0) - act.activate_linear_state("level", rate=-5.0, cause="TEST") + act.activate_linear_state("level", rate=-5.0, task=T) env.run(3.0) assert act.level == 90.0 env.run(5.0) @@ -113,28 +124,32 @@ class MyActor(Actor): def test_linear_multiple_activations() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) - + level: float = LinearChangingState(recording=True).create() + with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Changer", level=0.0) - act.activate_linear_state("level", rate=2.0, cause="TEST") + act.activate_linear_state("level", rate=2.0, task=T) env.run(2.0) assert act.level == 4.0 - act.deactivate_linear_state("level", cause="TEST") + act.deactivate_linear_state("level", task=T) env.run(4.0) assert act.level == 4.0 - act.activate_linear_state("level", rate=3.0, cause="TEST") + act.activate_linear_state("level", rate=3.0, task=T) env.run(6.0) assert act.level == 10.0 def test_linear_zero_rate() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Static", level=50.0) - act.activate_linear_state("level", rate=0.0, cause="TEST") + act.activate_linear_state("level", rate=0.0, task=T) env.run(10.0) assert act.level == 50.0 env.run(20.0) @@ -143,11 +158,13 @@ class MyActor(Actor): def test_linear_predict_negative_rate() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) - + level: float = LinearChangingState(recording=True).create() + with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Predictor", level=100.0) - act.activate_linear_state("level", rate=-2.0, cause="TEST") + act.activate_linear_state("level", rate=-2.0, task=T) field = act.__model_fields__["level"] assert isinstance(field, LinearChangingState) env.run(5.0) @@ -157,11 +174,13 @@ class MyActor(Actor): def test_linear_predict_past_value() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Predictor", level=10.0) - act.activate_linear_state("level", rate=5.0, cause="TEST") + act.activate_linear_state("level", rate=5.0, task=T) env.run(2.0) field = act.__model_fields__["level"] assert isinstance(field, LinearChangingState) @@ -170,14 +189,16 @@ class MyActor(Actor): def test_linear_state_history_deactivate() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Tracker", level=10.0) env.run(1.0) - act.activate_linear_state("level", rate=5.0, cause="TEST") + act.activate_linear_state("level", rate=5.0, task=T) env.run(3.0) - act.deactivate_linear_state("level", cause="TEST") + act.deactivate_linear_state("level", task=T) history = act._state_histories["level"] assert history[0] == (0.0, 10.0) assert history[1] == (1.0, 10.0, "ACTIVATING") @@ -186,11 +207,13 @@ class MyActor(Actor): def test_linear_get_updates_value() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Getter", level=0.0) - act.activate_linear_state("level", rate=10.0, cause="TEST") + act.activate_linear_state("level", rate=10.0, task=T) env.run(1.0) val1 = act.level val2 = act.level @@ -201,26 +224,34 @@ class MyActor(Actor): def test_linear_state_recording_false() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=False) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Untracked", level=5.0) - act.activate_linear_state("level", rate=2.0, cause="TEST") + act.activate_linear_state("level", rate=2.0, task=T) env.run(3.0) assert act.level == 11.0 - assert act._state_histories["level"] == deque([(0.0, 5.0, "ACTIVATING")]) + assert act._state_histories["level"] == deque([ + (0.0, 5.0), + (0.0, 5.0, "ACTIVATING"), + (3.0, 11), + ]) def test_make_event_for_state_goal() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Tester", level=10.0) - assert act.make_event_for_state_goal("level", 50.0) is None - act.activate_linear_state("level", rate=5.0, cause="TEST") + assert act.get_time_for_state_goal("level", 50.0) is None + act.activate_linear_state("level", rate=5.0, task=T) env.run(2.0) - goal_time = act.make_event_for_state_goal("level", 50.0) + goal_time = act.get_time_for_state_goal("level", 50.0) assert goal_time == 8.0 env.run(goal_time) assert act.level == 50.0 @@ -228,12 +259,14 @@ class MyActor(Actor): def test_make_event_for_state_goal_negative_rate() -> None: class MyActor(Actor): - level: float = LinearChangingState(recording=True) + level: float = LinearChangingState(recording=True).create() with EnvironmentContext() as env: + T = Task() + act = MyActor(name="Drainer", level=100.0) - act.activate_linear_state("level", rate=-10.0, cause="TEST") - goal_time = act.make_event_for_state_goal("level", 50.0) + act.activate_linear_state("level", rate=-10.0, task=T) + goal_time = act.get_time_for_state_goal("level", 50.0) assert goal_time == 5.0 env.run(goal_time) assert act.level == 50.0 diff --git a/tests/test_actor_clone.py b/tests/test_actor_clone.py deleted file mode 100644 index 7fece63..0000000 --- a/tests/test_actor_clone.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) - -# Licensed under the BSD 3-Clause License. -# See the LICENSE file in the project root for complete license terms and disclaimers. - -"""Test cloning actors.""" - -from upstage_des.api import Actor, EnvironmentContext, get_entities_by_class - - -def test_actor_clone_basic() -> None: - class Vehicle(Actor): - fuel: float = 100.0 - position: int = 0 - - with EnvironmentContext(): - vehicle = Vehicle(name="car1", fuel=50.0, position=10) - - cloned = vehicle.clone() - - assert cloned.name == "car1.clone" - assert cloned.fuel == 50.0 - assert cloned.position == 10 - assert cloned.is_clone is True - assert vehicle.is_clone is False - - -def test_actor_clone_deepcopy() -> None: - class Robot(Actor): - inventory: list[str] - position: tuple[int, int] - - with EnvironmentContext(): - robot = Robot(name="bot1", inventory=["item1", "item2"], position=(5, 10)) - - cloned = robot.clone() - - assert cloned.inventory == ["item1", "item2"] - assert cloned.inventory is not robot.inventory - - cloned.inventory.append("item3") - assert len(robot.inventory) == 2 - assert len(cloned.inventory) == 3 - robot._clean() - assert len(cloned.inventory) == 3 - - assert cloned.position == (5, 10) - - -def test_actor_clone_no_history() -> None: - class Ship(Actor): - speed: float = 0.0 - - with EnvironmentContext() as env: - ship = Ship(name="ship1", speed=10.0) - - assert "speed" in ship._state_histories - assert len(ship._state_histories["speed"]) == 1 - - ship.speed = 20.0 - assert len(ship._state_histories["speed"]) == 2 - - cloned = ship.clone() - - assert cloned.speed == 20.0 - assert len(cloned._state_histories) == 0 - - -def test_actor_clone_not_in_registry() -> None: - class Drone(Actor): - altitude: float = 0.0 - - with EnvironmentContext(): - drone = Drone(name="drone1", altitude=100.0) - - entities = get_entities_by_class("Drone") - assert len(entities) == 1 - assert drone in entities - - cloned = drone.clone() - - entities_after = get_entities_by_class("Drone") - assert len(entities_after) == 2 - assert drone in entities_after - assert cloned in entities_after - - -def test_actor_clone_inheritance() -> None: - class Vehicle(Actor): - fuel: float = 100.0 - - class Car(Vehicle): - passengers: int = 0 - - with EnvironmentContext(): - car = Car(name="sedan", fuel=75.0, passengers=3) - - cloned = car.clone() - - assert cloned.name == "sedan.clone" - assert cloned.fuel == 75.0 - assert cloned.passengers == 3 - assert cloned.is_clone is True - assert isinstance(cloned, Car) - assert isinstance(cloned, Vehicle) - - -def test_actor_clone_modified_after_creation() -> None: - class Tank(Actor): - ammo: int = 100 - health: float = 100.0 - - with EnvironmentContext() as env: - tank = Tank(name="tank1", ammo=50, health=75.0) - - env.run(env.timeout(5)) - tank.ammo = 25 - tank.health = 50.0 - - cloned = tank.clone() - - assert cloned.ammo == 25 - assert cloned.health == 50.0 - assert cloned.is_clone is True - - cloned.ammo = 100 - assert tank.ammo == 25 - assert cloned.ammo == 100 - - -def test_actor_clone_complex_nested_state() -> None: - class Agent(Actor): - data: dict[str, list[int]] - - with EnvironmentContext(): - agent = Agent(name="agent1", data={"scores": [1, 2, 3], "levels": [10, 20]}) - - cloned = agent.clone() - - assert cloned.data == {"scores": [1, 2, 3], "levels": [10, 20]} - assert cloned.data is not agent.data - - cloned.data["scores"].append(4) - assert agent.data["scores"] == [1, 2, 3] - assert cloned.data["scores"] == [1, 2, 3, 4] diff --git a/tests/test_actor_state.py b/tests/test_actor_state.py index 7291ca5..31ce1fe 100644 --- a/tests/test_actor_state.py +++ b/tests/test_actor_state.py @@ -5,6 +5,8 @@ """Test state features.""" +from typing import Annotated + import pytest from upstage_des.actor import Actor @@ -79,7 +81,7 @@ def validate_positive(obj: object, value: float) -> None: raise ValueError("Must be positive") class MyActor(Actor): - fuel: float = State(default=100.0, validator=validate_positive) + fuel: float = State[float](default=100.0, validator=validate_positive).create() with EnvironmentContext(): actor = MyActor(name="test") @@ -91,7 +93,7 @@ class MyActor(Actor): def test_state_default_factory() -> None: class MyActor(Actor): - items: list[int] = State(default_factory=list) + items: list[int] = State(default_factory=list).create() with EnvironmentContext(): actor1 = MyActor(name="test1") diff --git a/tests/test_knowledge.py b/tests/test_knowledge.py index 67d4c63..61853f8 100644 --- a/tests/test_knowledge.py +++ b/tests/test_knowledge.py @@ -5,37 +5,40 @@ """Test knowledge.""" -from typing import TypedDict +from dataclasses import dataclass +from typing import Any, TypedDict import pytest -from upstage_des.actor import EMPTY_KNOWLEDGE, Actor +from upstage_des.actor import EMPTY_KNOWLEDGE, Actor, Knowledge from upstage_des.base import EnvironmentContext, SimulationError +from upstage_des.states import State +from upstage_des.tasks import Task def test_knowledge() -> None: - class TD(TypedDict): + @dataclass + class TD(Knowledge): number: int message: float class MyActor(Actor): fuel: float = 120. - knowledge: TD + knowledge: TD = State(default_factory=TD.make_blank).create() class NoKnow(Actor):... with EnvironmentContext(): + ma_blank = MyActor(name="empty") + assert ma_blank.knowledge.number is EMPTY_KNOWLEDGE + assert ma_blank.knowledge.message is EMPTY_KNOWLEDGE + ma = MyActor( name="act", - knowledge={"number": 2, "message":3.0, "other":"string"} + knowledge=TD(number=2, message=3.0) ) - assert ma.knowledge["message"] == 3.0 + assert ma.knowledge.message == 3.0 assert ma.knowledge["number"] == 2 - assert ma.knowledge["other"] == "string" nk = NoKnow(name="no knowledge") - assert len(nk.knowledge) == 0 - nk.knowledge["new data"] = 2.3 - assert len(nk.knowledge) == 1 - v = ma.get_knowledge("message") assert v == 3.0 v = ma.get_knowledge("number", must_exist=True) @@ -53,7 +56,18 @@ class NoKnow(Actor):... ma.set_knowledge("number", 12.0, caller="The Test") assert ma.get_and_clear_knowledge("number") == 12.0 assert len(ma.get_log()) == 3 - assert "number" not in ma.knowledge + assert ma.knowledge.number is EMPTY_KNOWLEDGE + + # add back knowledge + ma.knowledge.number = 3 + ma.knowledge.message = 4.2 + # Clear it all w/ a task + t = Task() + assert ma.knowledge.number is not EMPTY_KNOWLEDGE + assert ma.knowledge.message is not EMPTY_KNOWLEDGE + t.clear_actor_bulk_knowledge(ma, ["number", "message"]) + assert ma.knowledge.number is EMPTY_KNOWLEDGE + assert ma.knowledge.message is EMPTY_KNOWLEDGE test_knowledge() diff --git a/tests/test_parallel_task_nets.py b/tests/test_parallel_task_nets.py new file mode 100644 index 0000000..85b511d --- /dev/null +++ b/tests/test_parallel_task_nets.py @@ -0,0 +1,89 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Testing for running more than one task network on an actor at a time.""" + +import simpy as SIM + +import upstage_des.api as UP +from upstage_des.api import Task, SIMPY_GEN, TASK_GEN + + +class ParallelTest(UP.Actor): + comms: SIM.Store = UP.State().create() + logger: list = UP.State().create() + internal: SIM.Store = UP.State().create() + working: bool = UP.State().create() + + +class TaskOne(Task): + def task(self, *, actor: ParallelTest) -> TASK_GEN: + actor.working = not actor.working + actor._lock_state(state="working", cause=self) + thing = yield UP.Get(actor.internal) + actor.logger.append((actor.env.now, thing, actor.working)) + actor._unlock_state(state="working", cause=self) + + +class TaskTwo(Task): + def task(self, *, actor: ParallelTest) -> TASK_GEN: + other = yield UP.Get(actor.comms) + actor.logger.append((actor.env.now, "Put the message", actor.working)) + yield UP.Put(actor.internal, other) + + +def test_parallel_looping() -> None: + with UP.EnvironmentContext() as env: + net_1_classes = {"Task": TaskOne} + net_1_links = {"Task": UP.TaskLinks(default="Task", allowed=["Task"])} + net_2_classes = {"Task": TaskTwo} + net_2_links = {"Task": UP.TaskLinks(default="Task", allowed=["Task"])} + + tn1 = UP.TaskNetwork("InternalGet", net_1_classes, net_1_links) + tn2 = UP.TaskNetwork("ExternalGet", net_2_classes, net_2_links) + + def proc(env: SIM.Environment, actor: ParallelTest, thing: str) -> SIMPY_GEN: + yield env.timeout(1.3) + yield actor.comms.put(thing) + yield env.timeout(2.2) + yield actor.comms.put(thing) + + pt = ParallelTest( + name="Parallel_Actor", + comms=SIM.Store(env), + internal=SIM.Store(env), + logger=[], + working=False, + ) + + pt.add_task_network(tn1) + pt.add_task_network(tn2) + + pt.start_network_loop("InternalGet", init_task_name="Task") + pt.start_network_loop("ExternalGet", init_task_name="Task") + env.process(proc(env, pt, "the msg")) + env.run(until=1.0) + running = pt.get_running_tasks() + assert len(running) == 2 + assert "InternalGet" in running + assert running["InternalGet"].name == "Task" + assert "ExternalGet" in running + assert running["ExternalGet"].name == "Task" + + tqs = pt.get_all_task_queues() + assert "InternalGet" in tqs + assert "ExternalGet" in tqs + + env.run() + + assert len(pt.logger) == 4 + expected_log = [ + (1.3, "Put the message", True), + (1.3, "the msg", True), + (3.5, "Put the message", False), + (3.5, "the msg", False), + ] + for el, al in zip(expected_log, pt.logger): + assert el == al diff --git a/tests/test_stage.py b/tests/test_stage.py index 5e52a15..19b0377 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -96,7 +96,8 @@ def test_stage_not_available_outside_context() -> None: def test_stage_property_error_outside_context() -> None: - base = UpstageBase() + with pytest.warns(UserWarning, match="Environment not created at instantiation"): + base = UpstageBase() with pytest.raises(LookupError): _ = base.stage diff --git a/tests/test_task_networks.py b/tests/test_task_networks.py new file mode 100644 index 0000000..0b02f1e --- /dev/null +++ b/tests/test_task_networks.py @@ -0,0 +1,787 @@ +# Copyright (C) 2026 by the Georgia Tech Research Institute (GTRI) + +# Licensed under the BSD 3-Clause License. +# See the LICENSE file in the project root for complete license terms and disclaimers. + +"""Test task networks.""" +import pytest + +from upstage_des.api import ( + Actor, + Any, + DecisionTask, + EnvironmentContext, + Event, + Get, + InterruptStates, + LinearChangingState, + Put, + ResourceHold, + State, + Task, + TaskLinks, + TaskNetworkFactory, + Wait, + WaitUntil, + add_stage_variable, + TASK_GEN, + SIMPY_GEN, + SimulationError, + UpstageError, + TaskTransition, + TerminalTask, + get_entities_by_class, + get_stage, + SimulationEnd, +) +from upstage_des.utils.task_net_viz import to_dot, to_mermaid + + +class Insect(Actor): + hunger: float = LinearChangingState(default=1.0, recording=True).create() + + +class InsectInterrupt(Task): + def on_interrupt(self, *, actor: Insect, cause: str) -> InterruptStates: + if cause == "attack": + self.clear_actor_task_queue(actor) + self.set_actor_task_queue(actor, ["Defend"]) + return InterruptStates.END + raise SimulationError(f"Unexpected cause: {cause}") + + +class Search(InsectInterrupt): + def task(self, *, actor: Insect) -> TASK_GEN: + # activate hunger to change over time + actor.activate_linear_state( + "hunger", + rate=-0.01, + task=self, # This has to be the task object + ) + no_food_time = actor.get_time_for_state_goal("hunger", 0.0) + assert no_food_time is not None + no_food_event = WaitUntil(no_food_time) + search_event = Wait.from_random_uniform(0.5, 2.0) + yield Any(no_food_event, search_event) + actor.deactivate_all_states(task=self) + if no_food_event.is_complete(): + self.set_actor_task_queue(actor, ["End"]) + else: + self.set_actor_task_queue(actor, ["Eat"]) + + +class Eat(InsectInterrupt): + def task(self, *, actor: Insect) -> TASK_GEN: + actor.activate_linear_state( + "hunger", + rate=0.1, + task=self, + ) + satiated_time = actor.get_time_for_state_goal("hunger", 1.0) + assert satiated_time is not None + yield WaitUntil(satiated_time) + actor.deactivate_linear_state("hunger", task=self) + + +class Sleep(InsectInterrupt): + def task(self, *, actor: Insect) -> TASK_GEN: + yield Wait(3.0) + actor.hunger -= 0.3 + + +class Defend(Task): + def task(self, *, actor: Insect) -> TASK_GEN: + yield Wait(0.2) + if actor.hunger < 0.3: + actor.hunger = 0.0 + self.set_actor_task_queue(actor, ["End"]) + else: + # Lose hunger, but win + actor.hunger -= 0.2 + + +class Think(DecisionTask): + def make_decision(self, *, actor: Insect) -> None: + if actor.hunger > 0.7: + # we can sleep! + self.set_actor_task_queue(actor, ["Sleep"]) + else: + self.set_actor_task_queue(actor, ["Search"]) + +class End(TerminalTask):... + + +class Swatter(Actor): ... + + +class SwatInsects(Task): + def task(self, *, actor: Swatter) -> TASK_GEN: + # Looping task for swatting insects + yield Wait.from_random_uniform(2.1, 4.3) + insects: list[Insect] = get_entities_by_class("Insect") + insects = [x for x in insects if x.hunger > 0] + if not insects: + raise SimulationEnd("No more insects") + choice = get_stage().random.choice(insects) + choice.interrupt_network("InsectLife", cause="attack") + + +def _make_sim(n_insects: int) -> tuple[list[Insect], Swatter]: + """Make a sim once inside a context.""" + tnf = TaskNetworkFactory( + name="InsectLife", + task_links={ + Think: TaskLinks(None, [Search, Sleep]), + Eat: TaskLinks(Think, [Think, Defend]), + Sleep: TaskLinks(Think, [Think]), + End: TaskLinks(None, []), + Search: TaskLinks(None, [Eat, Defend, End]), + Defend: TaskLinks(Think, [Think, End]), + } + ) + + tnfs = TaskNetworkFactory.from_single_looping( + "SwatThatInsect", SwatInsects, + ) + + insects = [ + Insect( + name=f"Ant {i}", + ) + for i in range(n_insects) + ] + for ins in insects: + net = tnf.make_network() + ins.add_task_network(net) + ins.start_network_loop(net.name, "Think") + + swatter = Swatter(name="Swat") + net = tnfs.make_network() + swatter.add_task_network(net) + swatter.start_network_loop(net.name, "SwatInsects") + return insects, swatter + + +def test_building_network() -> None: + with EnvironmentContext() as env: + insects, swatter = _make_sim(2) + try: + env.run() + except SimulationEnd: + ... + assert env.now > 0.0 + + +def test_factory_builders() -> None: + + class AnActor(Actor): + data: int = 0 + + class TaskA(Task): + def task(self, *, actor: AnActor) -> TASK_GEN: + actor.data += 1 + yield Wait(1.0) + + class TaskB(Task): + def task(self, *, actor: AnActor) -> TASK_GEN: + actor.data += 3 + yield Wait(2.0) + + net_single_term_A = TaskNetworkFactory.from_single_terminating( + "termA", + TaskA, + ) + + net_single_term_B = TaskNetworkFactory.from_single_terminating( + "termB", + TaskB, + ) + + net_looping = TaskNetworkFactory.from_ordered_loop( + "Loop", + [TaskA, TaskB], + ) + + net_order_term = TaskNetworkFactory.from_ordered_terminating( + "OrderTerm", + [TaskA, TaskB], + ) + + # Test the singles + with EnvironmentContext() as env: + act = AnActor(name="example") + net = net_single_term_A.make_network() + act.add_task_network(net) + act.start_network_loop(net.name, "TaskA") + env.run() + assert env.now == 1 + assert act.data == 1 + + with EnvironmentContext() as env: + act = AnActor(name="example") + net = net_single_term_B.make_network() + act.add_task_network(net) + act.start_network_loop(net.name, "TaskB") + env.run() + assert env.now == 2 + assert act.data == 3 + + # Test the loops + with EnvironmentContext() as env: + act = AnActor(name="example") + net = net_looping.make_network() + act.add_task_network(net) + act.start_network_loop(net.name, "TaskA") + env.run(until=4.1) + # 1 adds 2, 2 adds 3, 1 adds 1, then 3 added again before yield + assert act.data == 8 + + # Test the loops, but it starts on a different one + with EnvironmentContext() as env: + act = AnActor(name="example") + net = net_looping.make_network() + act.add_task_network(net) + act.start_network_loop(net.name, "TaskB") + env.run(until=4.1) + # 2 adds 3, 1 adds 1, 2 adds 3 + assert act.data == 7 + + # Test ordered terminating + with EnvironmentContext() as env: + act = AnActor(name="example") + net = net_order_term.make_network() + act.add_task_network(net) + act.start_network_loop(net.name, "TaskA") + env.run() + assert env.now == 3 + assert act.data == 4 + + +def test_network_naming() -> None: + class AnActor(Actor): + data: int = 0 + + class TaskA(Task): + def task(self, *, actor: AnActor) -> TASK_GEN: + actor.data += 1 + yield Wait(1.0) + + netfact = TaskNetworkFactory.from_single_looping("TASKA", TaskA) + + with EnvironmentContext() as env: + act = AnActor(name="example") + net_name = act.suggest_network_name(netfact) + assert net_name == "TASKA" + act.add_task_network(netfact.make_network()) + net_name2 = act.suggest_network_name(netfact) + assert net_name2 == "TASKA_1" + act.delete_task_network(net_name) + net_name3 = act.suggest_network_name(netfact) + assert net_name3 == "TASKA" + + +def test_decision_task_hold() -> None: + # Test the conditions found in https://github.com/gtri/upstage/issues/35 + # Looks at zero time holds vs pass-through decision tasks + + # Test for new behavior first. + data = [] + + class Waiter(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + data.append(f"{self.env.now:.1f} >> {actor.name} in Waiter") + yield Wait(1.0) + + class Runner(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + data.append(f"{self.env.now:.1f} >> {actor.name} in Runner") + yield Wait(2.0) + + class Thinker(DecisionTask): + DO_NOT_HOLD = True + + def make_decision(self, *, actor: Actor) -> None: + data.append(f"{self.env.now:.1f} >> {actor.name} in Thinker") + if "one" in actor.name: + self.set_actor_task_queue(actor, ["Waiter"]) + else: + self.set_actor_task_queue(actor, ["Runner"]) + + net = TaskNetworkFactory( + name="Example Net", + task_classes={"Waiter": Waiter, "Runner": Runner, "Thinker": Thinker}, + task_links={ + "Waiter": TaskLinks(default="Thinker", allowed=["Thinker"]), + "Thinker": TaskLinks(default="", allowed=["Waiter", "Runner"]), + "Runner": TaskLinks(default="Thinker", allowed=["Thinker"]), + }, + ) + with EnvironmentContext() as env: + a = Actor(name="Actor one", debug_logging=True) + b = Actor(name="Actor two", debug_logging=True) + + for actor in [a, b]: + n = net.make_network() + actor.add_task_network(n) + actor.start_network_loop(n.name, "Waiter") + + env.run(until=2) + + expected = [ + "0.0 >> Actor one in Waiter", + "0.0 >> Actor two in Waiter", + "1.0 >> Actor one in Thinker", + "1.0 >> Actor one in Waiter", + "1.0 >> Actor two in Thinker", + "1.0 >> Actor two in Runner", + ] + assert data == expected + + # Reset data in place, test for default behavior + data[:] = [] + + Thinker.DO_NOT_HOLD = False + with EnvironmentContext() as env: + a = Actor(name="Actor one", debug_logging=True) + b = Actor(name="Actor two", debug_logging=True) + + for actor in [a, b]: + n = net.make_network() + actor.add_task_network(n) + actor.start_network_loop(n.name, "Waiter") + + env.run(until=2) + expected = [ + "0.0 >> Actor one in Waiter", + "0.0 >> Actor two in Waiter", + "1.0 >> Actor one in Thinker", + "1.0 >> Actor two in Thinker", + "1.0 >> Actor one in Waiter", + "1.0 >> Actor two in Runner", + ] + assert data == expected + + +# ---- class-reference API and validation tests ---- + + +def test_class_keyed_factory() -> None: + class Mover(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class Planner(DecisionTask): + def make_decision(self, *, actor: Actor) -> None: + pass + + factory = TaskNetworkFactory( + "ClassNet", + task_links={ + Mover: TaskLinks(default=Planner, allowed=[Planner]), + Planner: TaskLinks(default=Mover, allowed=[Mover]), + }, + ) + assert "Mover" in factory.task_classes + assert "Planner" in factory.task_classes + assert factory.task_links["Mover"].default == "Planner" + assert factory.task_links["Planner"].allowed == ["Mover"] + + with EnvironmentContext() as env: + a = Actor(name="test") + net = factory.make_network() + a.add_task_network(net) + a.start_network_loop(net.name, "Mover") + env.run(until=3) + + +def test_class_keyed_single_arg() -> None: + """Class-keyed map passed as the *first* positional arg (task_classes slot).""" + + class Ping(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + links: dict[type[Task], TaskLinks] = {Ping: TaskLinks(default=Ping, allowed=[Ping])} + factory = TaskNetworkFactory( + "PingNet", + links, + ) + assert "Ping" in factory.task_classes + + +def test_validation_catches_bad_reference() -> None: + class Good(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + bad_links: dict[type[Task], TaskLinks] = { + Good: TaskLinks(default="Nonexistent", allowed=["Nonexistent"]), + } + with pytest.raises(UpstageError, match="unknown task name"): + TaskNetworkFactory("Bad", task_links=bad_links) + + +def test_validation_warns_unlinked_task() -> None: + class A(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class B(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + with pytest.warns(UserWarning, match="no entry in task_links"): + TaskNetworkFactory( + "Partial", + task_classes={"A": A, "B": B}, + task_links={"A": TaskLinks(default="A", allowed=["A"])}, + ) + + +def test_string_api_still_works() -> None: + """Existing string-keyed API is unchanged.""" + + class X(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory( + "Compat", + task_classes={"X": X}, + task_links={"X": TaskLinks(default="X", allowed=["X"])}, + ) + assert factory.task_classes["X"] is X + + +# ---- guard-based transitions, on_enter/on_exit, and visualization tests ---- + + +def test_guard_transitions() -> None: + """Guards determine the next task when the current one finishes.""" + trace: list[str] = [] + + class Bot(Actor): + status: int = State().create() + + class StepA(Task): + def task(self, *, actor: Bot) -> TASK_GEN: + trace.append("A") + yield Wait(1.0) + actor.status += 1 + + class StepB(Task): + def task(self, *, actor: Bot) -> TASK_GEN: + trace.append("B") + yield Wait(1.0) + + class StepC(Task): + def task(self, *, actor: Bot) -> TASK_GEN: + trace.append("C") + yield Wait(1.0) + + def go_to_c(actor: Bot) -> bool: + return actor.status >= 2 + + factory = TaskNetworkFactory( + "GuardNet", + task_links={ + StepA: TaskLinks( + transitions=[ + TaskTransition(StepC, go_to_c), + TaskTransition(StepB, None), # fallback + ] + ), + StepB: TaskLinks(transitions=[TaskTransition(StepA, None)]), + StepC: TaskLinks(transitions=[TaskTransition(StepA, None)]), + }, + ) + + with EnvironmentContext() as env: + bot = Bot(name="bot", status=0) + net = factory.make_network() + bot.add_task_network(net) + bot.start_network_loop(net.name, "StepA") + env.run(until=10) + + # status increments each time StepA runs (at the end of the task) + # A(status 0→1): guard false → B, A(status 1→2): guard true → C, A(2→3) → C ... + assert trace[:6] == ["A", "B", "A", "C", "A", "C"] + + +def test_on_enter_on_exit() -> None: + """on_enter runs before task(), on_exit runs after.""" + trace: list[str] = [] + + class Greeter(Task): + def on_enter(self, *, actor: Actor) -> None: + trace.append("enter") + + def task(self, *, actor: Actor) -> TASK_GEN: + trace.append("task") + yield Wait(1.0) + + def on_exit(self, *, actor: Actor) -> None: + trace.append("exit") + + factory = TaskNetworkFactory.from_single_looping("Loop", Greeter) + with EnvironmentContext() as env: + a = Actor(name="a") + net = factory.make_network() + a.add_task_network(net) + a.start_network_loop(net.name, "Greeter") + env.run(until=2.5) + + # Two full cycles (enter/task/exit) + a third enter/task in-flight at t=2.5 + assert trace == ["enter", "task", "exit", "enter", "task", "exit", "enter", "task"] + + +def test_to_mermaid() -> None: + class A(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class B(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory( + "Viz", + task_links={ + A: TaskLinks( + transitions=[ + TaskTransition(B, lambda actor: True), + TaskTransition(A, None), + ] + ), + B: TaskLinks(transitions=[TaskTransition(A, None)]), + }, + ) + net = factory.make_network() + mermaid = to_mermaid(net) + assert "graph TD" in mermaid + assert "A" in mermaid + assert "B" in mermaid + # Factory passthrough also works + assert to_mermaid(factory) == mermaid + + +def test_to_dot() -> None: + class X(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class Y(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory( + "DotViz", + task_links={ + X: TaskLinks(default=Y, allowed=[Y]), + Y: TaskLinks(default=X, allowed=[X]), + }, + ) + net = factory.make_network() + dot = to_dot(net) + assert "digraph" in dot + assert '"X"' in dot + assert '"Y"' in dot + # Factory passthrough also works + assert to_dot(factory) == dot + + +# Snapshot tests for the diagram generators. These are golden-file tests: +# they pin the exact string output so that format changes are caught and +# have to be updated explicitly. If you are deliberately changing the +# Mermaid or DOT output, update the expected strings below to match. + + +def test_to_mermaid_snapshot_guards_and_labels() -> None: + """Full Mermaid output with guard labels and uniform node declarations.""" + + class A(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class B(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + def needs_b(actor: Actor) -> bool: + return True + + factory = TaskNetworkFactory( + "Snap", + task_links={ + A: TaskLinks( + transitions=[ + TaskTransition(B, needs_b, "go to B"), + TaskTransition(A, None), + ] + ), + B: TaskLinks(transitions=[TaskTransition(A, None)]), + }, + ) + expected = "\n".join( + [ + "graph TD", + ' A["A"]', + ' B["B"]', + " A -->|go to B| B", + " A --> A", + " B --> A", + ] + ) + net = factory.make_network() + assert to_mermaid(net) == expected + + +def test_to_mermaid_snapshot_allowed_edges_and_legend() -> None: + """Allowed-only edges produce dashed lines and a legend.""" + + class A(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class B(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory( + "SnapAllowed", + task_links={ + A: TaskLinks(default=A, allowed=[A, B]), + B: TaskLinks(default=A, allowed=[A]), + }, + ) + mermaid = to_mermaid(factory.make_network()) + # Uniform nodes + assert ' A["A"]' in mermaid + assert ' B["B"]' in mermaid + # Default edges + assert " A --> A" in mermaid + assert " B --> A" in mermaid + # Allowed-only (dashed) edge + assert " A -.-> B" in mermaid + # Legend triggered by dashed edges + assert "subgraph Legend" in mermaid + assert "|transition|" in mermaid + assert "|via queue|" in mermaid + + +def test_to_mermaid_snapshot_legend_off() -> None: + """`legend=False` suppresses the legend even when dashed edges exist.""" + + class A(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class B(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory( + "SnapNoLegend", + task_links={ + A: TaskLinks(default=A, allowed=[A, B]), + B: TaskLinks(default=A, allowed=[A]), + }, + ) + mermaid = to_mermaid(factory.make_network(), legend=False) + assert "subgraph Legend" not in mermaid + assert " A -.-> B" in mermaid + + +def test_to_mermaid_snapshot_with_hooks() -> None: + """Tasks defining on_enter/on_exit get annotated nodes.""" + + class Hooked(Task): + def on_enter(self, *, actor: Actor) -> None: + pass + + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory.from_single_looping("Hook", Hooked) + mermaid = to_mermaid(factory.make_network()) + assert ' Hooked["Hooked
(on_enter)"]' in mermaid + + +def test_to_dot_snapshot_uniform_nodes() -> None: + """DOT output declares every task node, hooked or not.""" + + class X(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class Y(Task): + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + factory = TaskNetworkFactory( + "DotSnap", + task_links={ + X: TaskLinks(default=Y, allowed=[Y]), + Y: TaskLinks(default=X, allowed=[X]), + }, + ) + expected = "\n".join( + [ + "digraph DotSnap {", + " rankdir=TB;", + ' node [shape=box, style=rounded, fontname="Helvetica"];', + ' edge [fontname="Helvetica", fontsize=10];', + "", + ' "X";', + ' "Y";', + "", + ' "X" -> "Y";', + ' "Y" -> "X";', + "}", + ] + ) + assert to_dot(factory.make_network()) == expected + + +def test_to_dot_snapshot_with_hooks_and_labels() -> None: + """DOT output with hooks, guard labels, and dashed (allowed-only) edges.""" + + class Enter(Task): + def on_enter(self, *, actor: Actor) -> None: + pass + + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + class Exit(Task): + def on_exit(self, *, actor: Actor) -> None: + pass + + def task(self, *, actor: Actor) -> TASK_GEN: + yield Wait(1.0) + + def done(actor: Actor) -> bool: + return True + + factory = TaskNetworkFactory( + "DotFull", + task_links={ + Enter: TaskLinks( + transitions=[TaskTransition(Exit, done, "ready")], + allowed=[Enter], + ), + Exit: TaskLinks(default=Enter, allowed=[Enter]), + }, + ) + dot = to_dot(factory.make_network()) + assert ( + ' "Enter" [label=(on_enter)>];' + in dot + ) + assert ( + ' "Exit" [label=(on_exit)>];' + in dot + ) + assert ' "Enter" -> "Exit" [label="ready"];' in dot + assert ' "Enter" -> "Enter" [style=dashed];' in dot + assert ' "Exit" -> "Enter";' in dot diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 6b5ac1b..60b42d6 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -5,31 +5,33 @@ """Test singular tasks.""" +from dataclasses import dataclass from inspect import isgeneratorfunction from typing import Any, TypedDict, cast import pytest from simpy import Environment, Interrupt, Process -from upstage_des.actor import Actor +from upstage_des.actor import EMPTY_KNOWLEDGE, Actor, Knowledge from upstage_des.base import SIMPY_GEN, EnvironmentContext, SimulationError from upstage_des.events import Wait from upstage_des.states import LinearChangingState, State from upstage_des.tasks import DecisionTask, InterruptStates, Task, TASK_GEN, TerminalTask -class Know(TypedDict): +@dataclass +class Know(Knowledge): thing1: str thing2: float class TaskedActor(Actor): time: float - knowledge: Know + knowledge: Know = State(default_factory=Know.make_blank).create() class KnowTask(Task): """A task for testing.""" - def task(self, *, actor: Actor) -> TASK_GEN: + def task(self, *, actor: TaskedActor) -> TASK_GEN: self.set_actor_bulk_knowledge( actor, know = {"thing1": "Hello", "thing2": 6.28}, @@ -44,17 +46,17 @@ def task(self, *, actor: Actor) -> TASK_GEN: class ActorForTest(Actor): - dummy: float = State(recording=True) + dummy: float = State(recording=True).create() class ActorChangeForTest(Actor): - dummy: float = LinearChangingState() + dummy: float = LinearChangingState().create() class Dummy(Actor): status: str rate: float - changer: float = LinearChangingState(recording=True) + changer: float = LinearChangingState(recording=True).create() class WorkingTask(Task): @@ -74,9 +76,9 @@ class ChangingTask(Task): def task(self, *, actor: ActorForTest) -> TASK_GEN: for t in self.times: the_event = Wait(t) - actor.activate_state(state="dummy", cause=self, rate=self.rate) + actor.activate_state(state="dummy", task=self, rate=self.rate) yield the_event - actor.deactivate_state(state="dummy", cause=self) + actor.deactivate_state(state="dummy", task=self) class Actor2Test(Actor): @@ -111,7 +113,7 @@ class ChangingTask2(Task): def task(self, *, actor: ActorForTest | ActorChangeForTest) -> TASK_GEN: for wait_period in self.times: wait_event = Wait(wait_period) - actor.activate_state(state="dummy", cause=self, rate=self.rate) + actor.activate_state(state="dummy", task=self, rate=self.rate) actor.set_knowledge("example for logging", "a value", overwrite=True) self.log.append( f"{self.env.now}: {self.__class__.__name__} " @@ -122,11 +124,11 @@ def task(self, *, actor: ActorForTest | ActorChangeForTest) -> TASK_GEN: f"{self.env.now}: {self.__class__.__name__} finished " f"waiting {wait_period}, value={actor.dummy}" ) - actor.deactivate_state(state="dummy", cause=self) + actor.deactivate_state(state="dummy", task=self) def _task_runner(env: Environment, rate: float, timeout_point: float, final: bool=False) -> SIMPY_GEN: - use_actor = ActorChangeForTest(name="testing", dummy=0.0, debug_log=True) + use_actor = ActorChangeForTest(name="testing", dummy=0.0, debug_logging=True) times = [1.0, 2.0] task_object = ChangingTask2() @@ -170,7 +172,7 @@ def test_failures_for_tasks_with_simpy_events() -> None: class BrokenTask(Task): def task(self, *, actor: ActorForTest) -> TASK_GEN: - yield self.env.timeout(1.0) # type: ignore [misc, union-attr] + yield self.env.timeout(1.0) # type: ignore [misc] # msg = "*Task is yielding objects without `as_event`*" with pytest.raises(SimulationError): # , match=msg): @@ -180,13 +182,6 @@ def task(self, *, actor: ActorForTest) -> TASK_GEN: ) env.run() - # msg = "*'MockEnvironment' object has no attribute 'timeout'*" - with pytest.raises(AttributeError): # , match=msg): - the_task = BrokenTask() - the_task.rehearse( - actor=actor, - ) - def test_failures_for_tasks_with_incorrect_events() -> None: with EnvironmentContext(): @@ -332,12 +327,12 @@ class Restartable(Task): def task(self, *, actor: Dummy) -> TASK_GEN: actor.activate_state( state="changer", - cause=self, + task=self, rate=actor.rate, ) self.set_marker("change to test") yield Wait(10.0) - actor.deactivate_all_states(cause=self) + actor.deactivate_all_states(task=self) def on_interrupt(self, *, actor: Dummy, cause: Any) -> InterruptStates: if cause == "restart": @@ -353,7 +348,7 @@ def test_restart() -> None: status="available", rate=2.3, changer=0.0, - debug_log=True, + debug_logging=True, ) task = Restartable() @@ -377,7 +372,7 @@ class Dummy(Actor): status: Any = State() with EnvironmentContext() as env: - actor = Dummy(name="x", status="Good", debug_log=True) + actor = Dummy(name="x", status="Good", debug_logging=True) task = EndPoint() proc = task.run(actor=actor) @@ -390,7 +385,7 @@ class Dummy(Actor): proc.interrupt() env.run() - actor = Dummy(name="x", status="Good", debug_log=True) + actor = Dummy(name="x", status="Good", debug_logging=True) task = EndPointBase() proc = task.run(actor=actor) env.run() @@ -400,19 +395,19 @@ class Dummy(Actor): # See if the final interrupt value keeps it from failing. with EnvironmentContext() as env: - actor = Dummy(name="x", status="Good", debug_log=True) - task = EndPoint() + actor = Dummy(name="x", status="Good", debug_logging=True) + task_two = EndPoint() - proc = task.run(actor=actor) + proc = task_two.run(actor=actor) env.run() - task._final_interrupt = True + task_two._final_interrupt = True proc.interrupt(cause="FINAL") env.run() def test_markers() -> None: class MarkedTask(Task): - def task(self, *, actor: Actor): + def task(self, *, actor: Actor) -> TASK_GEN: self.set_marker("First", InterruptStates.IGNORE) yield Wait(1.0) self.clear_marker() @@ -451,8 +446,8 @@ def task(self, *, actor: Actor): def test_interrupt_process() -> None: - data = [] - def proc(env, t: float): + data: list[Any] = [] + def proc(env: Environment, t: float) -> SIMPY_GEN: data.append("start") try: yield env.timeout(t) @@ -460,45 +455,53 @@ def proc(env, t: float): except Interrupt as e: data.append(e.cause) - def proc_bad(env, t: float): + def proc_bad(env: Environment, t: float) -> SIMPY_GEN: data.append("start") yield env.timeout(t) data.append("done") class ProcTask(Task): - def task(self, *, actor: Actor): + def task(self, *, actor: Actor) -> TASK_GEN: _p = self.env.process(proc(self.env, 2.1)) - yield _p + yield _p # type: ignore[misc] class ProcTaskBad(Task): - def task(self, *, actor: Actor): + def task(self, *, actor: Actor) -> TASK_GEN: _p = self.env.process(proc_bad(self.env, 2.1)) - yield _p + yield _p # type: ignore[misc] - with EnvironmentContext() as env: - act = Actor(name="example") - t = ProcTask() - p = t.run(actor=act) - env.run(until=1.3) - assert data[0] == "start" - p.interrupt(cause="CAUSE") - env.run() - assert data[-1] == "CAUSE" - - with EnvironmentContext() as env: - act = Actor(name="example") - t = ProcTaskBad() - p = t.run(actor=act) - env.run(until=1.3) - assert data[0] == "start" - p.interrupt(cause="CAUSE") - with pytest.raises(Interrupt): + with pytest.warns(UserWarning, match="Yielding a simpy.Process"): + with EnvironmentContext() as env: + act = Actor(name="example") + t = ProcTask() + p = t.run(actor=act) + env.run(until=1.3) + assert data[0] == "start" + p.interrupt(cause="CAUSE") env.run() + assert data[-1] == "CAUSE" + + with EnvironmentContext() as env: + act = Actor(name="example") + t2 = ProcTaskBad() + p = t2.run(actor=act) + env.run(until=1.3) + assert data[0] == "start" + p.interrupt(cause="CAUSE") + with pytest.raises(Interrupt): + env.run() +@dataclass +class ExamKnow(Knowledge): + exam: str + +class KnowActor(Actor): + knowledge: ExamKnow = State(default_factory=ExamKnow.make_blank).create() + def test_decision_task() -> None: class DT(DecisionTask): - def make_decision(self, *, actor: Actor): + def make_decision(self, *, actor: KnowActor) -> None: self.set_actor_knowledge( actor, "exam", @@ -506,7 +509,7 @@ def make_decision(self, *, actor: Actor): ) with EnvironmentContext() as env: - act = Actor(name="Example") + act = KnowActor(name="Example") dt = DT() dt.run(actor=act) env.run() @@ -515,8 +518,16 @@ def make_decision(self, *, actor: Actor): def test_task_knowledge() -> None: + @dataclass + class Know(Knowledge): + ONE: int + TWO: float + + class KnowAct(Actor): + knowledge: Know = State(default_factory=Know.make_blank).create() + class KnowTask2(Task): - def task(self, *, actor: Actor): + def task(self, *, actor: KnowAct) -> TASK_GEN: self.set_actor_bulk_knowledge( actor=actor, know={"ONE": 1, "TWO": 2.0} @@ -529,15 +540,15 @@ def task(self, *, actor: Actor): assert ans == {"TWO": 2} with EnvironmentContext() as env: - act = Actor(name="Example") + act = KnowAct(name="Example") kt2 = KnowTask2() kt2.run(actor=act) env.run(until=0.8) assert "ONE" in act.knowledge assert "TWO" in act.knowledge env.run(until=1.2) - assert "ONE" not in act.knowledge - assert "TWO" in act.knowledge + assert act.knowledge.ONE is EMPTY_KNOWLEDGE + assert act.knowledge.TWO is not EMPTY_KNOWLEDGE env.run(until=2.2) - assert "ONE" not in act.knowledge - assert "TWO" not in act.knowledge + assert act.knowledge.ONE is EMPTY_KNOWLEDGE + assert act.knowledge.TWO is EMPTY_KNOWLEDGE