From 076315248052a1936e19babbcb5858d17de6602f Mon Sep 17 00:00:00 2001 From: Matt Richards Date: Tue, 5 May 2026 20:46:32 +1000 Subject: [PATCH 1/2] Squashed commit of the following: (This is the squashed version of merge request https://github.com/VictoriaTransport/activitysim/pull/7 ) commit 5c2f84f0ceb02ba264bd5148b914fa0ae1d0b8b2 Author: Matt Richards Date: Thu Mar 5 10:13:15 2026 +1100 add chunk size logging commit a74644e38d4166f8e44d591f249a5414e809a8d5 Author: Matt Richards Date: Wed Mar 4 19:24:07 2026 +1100 fix again commit 5723f6dfae7a70cee54aeebe0e4c442faf7d08d0 Author: Matt Richards Date: Wed Mar 4 11:24:49 2026 +1100 fix sampling probs commit ced41c39e0a6fe2f1cc52f92514a789dd8494380 Author: Matt Richards Date: Tue Mar 3 12:36:13 2026 +1100 fix commit 264d5e6e145917b626de16e525b8585b5a44a0df Author: Matt Richards Date: Tue Mar 3 12:00:06 2026 +1100 revise handling of zone system offset commit 1492ff04358b74babe7837246ae03646a24a7dea Author: Matt Richards Date: Fri Feb 27 09:20:35 2026 +1100 fix typo commit 923ecef06d3df6eb1db353d127098edaf4c6b5b1 Author: Matt Richards Date: Thu Feb 26 21:45:56 2026 +1100 more stingy with memory commit 1cf695270d31f3f33af0e24c2cd1ed9f59630c13 Author: Matt Richards Date: Thu Feb 26 21:43:52 2026 +1100 in principle trip scheduling choice chunking commit 8721a6fa1e1a33e759185a7cd0fa91a1354d3c28 Author: Matt Richards Date: Thu Feb 26 12:36:40 2026 +1100 rng indexing fix commit 97a6f8110d4d8d459968a5980be32d86aea177bf Author: Matt Richards Date: Wed Feb 25 09:33:37 2026 +1100 add the shadow pricing fix commit 776d115801f85d132d03aa7fc6f396bdfb458eee Author: Matt Richards Date: Tue Feb 24 20:15:26 2026 +1100 REF: assorted cleanups commit 247c1be9032dce8be4f33b44dd08476e3474a243 Author: Matt Richards Date: Tue Feb 24 12:37:38 2026 +1100 BUG: POC solution to frozen randomness commit 2cf7e698f543d4ae9a65d33813790d67a8cd5aeb Author: Matt Richards Date: Mon Feb 23 17:04:53 2026 +1100 TST: document existing deficiency commit 181abe1172e092a2790ae44fb80c52b0007a68cd Author: Matt Richards Date: Fri Feb 20 01:01:31 2026 +1100 note commit b9432ebc1b023253f1f53cdecda540c0a2f8424d Author: Matt Richards Date: Fri Feb 20 00:19:11 2026 +1100 extra stability for small alternative sets commit 87ff2598bca6c58bdc4b6bda9ded26e1cb808bff Author: Matt Richards Date: Thu Feb 19 17:27:40 2026 +1100 tidy up commit a0663a616ade3973da551ed971528a28f34c50b4 Author: Matt Richards Date: Tue Feb 17 13:52:43 2026 +1100 restructure to avoid order reshuffling commit bb7be685a4d07c3d7ad4e4d61326269dc04a395a Author: Matt Richards Date: Mon Feb 16 15:14:07 2026 +1100 implement poisson sampling commit d115b3fbdafd2c192954816d6657c3b1c4f69c6a Author: Matt Richards Date: Tue Feb 3 21:32:08 2026 +1000 minor type checking commit 71d9481b175f9caf061c2180171ce619b49d6756 Author: Matt Richards Date: Tue Feb 3 21:20:12 2026 +1000 fix zero probs with new arg types commit 4f67fc6f7a1fce190c08358430a17779b58893f4 Merge: 52156307e 2a8c8829f Author: Matt Richards Date: Fri Jan 23 11:43:04 2026 +1000 Merge branch 'matt/explicit_error_terms_update_v151' into matt/eet_on_v24_1_5_2 commit 2a8c8829fb305989dc15fdc0c444b828689449f7 Author: Matt Richards Date: Mon Jan 12 22:02:45 2026 +1100 make estimation choice set preservation work for eet commit 14fe80d659a5d84d44c987df9ddd8e90a0229288 Author: Matt Richards Date: Mon Jan 12 21:56:27 2026 +1100 refactor estimation mode into method commit 2b4eb81cfdfa06836ea7270bb00801c94e3dd131 Merge: 6f31c76db 5963d038c Author: Matt Richards Date: Fri Jan 23 11:40:01 2026 +1000 Merge tag 'v1.5.1' into matt/explicit_error_terms_update_v151 # Conflicts: # activitysim/core/configuration/top.py # activitysim/core/interaction_sample.py commit 6f31c76dbb608468b0dfd86a0253827fca968c8b Author: Jan Zill Date: Fri May 30 13:11:31 2025 +1000 fix allow_zero_probs for prob calculation in EET interaction_sample commit 3b48a076636cd0afc6d1ae19c0f7de6fa4638661 Author: Jan Zill Date: Fri May 30 10:26:11 2025 +1000 lint commit 66cdac906395d70e52ab2970e3fb5af86b59dfcb Author: Jan Zill Date: Fri May 30 10:23:42 2025 +1000 consistently treat utilities that would lead to zero choices in MC simulation commit e102a9e54dbfb9e3dc663c4da06cde840fd42a86 Author: Jan Zill Date: Fri May 30 09:17:47 2025 +1000 adds validate utility method to align with unavailable choices in MC simulation commit 94e629ace077e64fbfa9db74da89c8ca53e7ec03 Author: Jan Zill Date: Thu May 29 22:10:43 2025 +1000 patch other instance of padded_utility for eet commit 5636cd9025a012c2b53891234309949cdb11885d Author: Jan Zill Date: Thu May 29 21:09:25 2025 +1000 lint commit c40b989f717176f227c4705d09d50412899b49dd Author: Jan Zill Date: Thu May 29 20:55:47 2025 +1000 avoid choosing alternatives corresponding to padded utilities in interaction_sample_simulate commit 49fcaa5519e8c5462db2fa92260f6823834f5cd1 Merge: 582c998f5 146c7ffee Author: Jan Zill Date: Thu May 29 14:51:11 2025 +1000 Merge remote-tracking branch 'origin/master' into janzill/utilit_based_choices_2025 commit 582c998f516ab3e26fa888a6de7fd8282d945926 Author: Jan Zill Date: Mon May 26 10:52:14 2025 +1000 comment clean up commit 41d49ffd3d7eff90420708abd6927937e20f456c Author: Jan Zill Date: Mon May 26 10:39:59 2025 +1000 clean up comments commit 1f815e82a78b042da88e85a0cb8daf81896f511f Author: Jan Zill Date: Mon May 26 10:24:21 2025 +1000 remove comment commit ef0be214a901418e6a58c1eb946a5328bcaa7239 Author: Jan Zill Date: Mon May 26 10:06:06 2025 +1000 fix nest_spec is None bug commit 2a563ff1ea2b3e869c87022f17e5bf9455db35bb Author: Jan Zill Date: Mon May 26 08:39:50 2025 +1000 lint commit 5de1d23903677e5c744c95517e2804b768df4717 Author: Jan Zill Date: Mon May 26 08:29:10 2025 +1000 compute settings in disaggreagte accessibility w/o hack commit 28142cdf2cbf63a70ec38b16454c14273659bcb6 Author: Jan Zill Date: Mon May 26 07:59:04 2025 +1000 fix bool conversion of None for eet compute settings in sub-components commit fc686978108f018a433fd9d491cae6d88a443506 Author: Jan Zill Date: Mon May 26 07:40:31 2025 +1000 fix _interaction_sample return doc commit 3793ec24442b9dbf759d8f900b5bb4a684f0a283 Author: Jan Zill Date: Mon May 26 07:36:30 2025 +1000 fix no sampling in interaction_sample commit d6ecb0d9a1b9670fe2122f2a05db3953edd39f3a Author: Jan Zill Date: Mon May 26 06:51:17 2025 +1000 update comments commit 892c3fbe8d2b2f29f1549804f14c5b8c70608496 Author: Jan Zill Date: Mon May 26 06:39:37 2025 +1000 move interaction sample without sampling out of MC loop to enable for both MC and eet commit 9900f48486282d9017109782308396dfe5487026 Author: Jan Zill Date: Sun May 25 19:23:41 2025 +1000 lint commit 0fa183ec26ad8e564b862054a0d3a38a0d285ad8 Author: Jan Zill Date: Sun May 25 11:15:07 2025 +1000 jtp custom chooser for EET commit 9a0fb663dc11e68b288d067aaa5916930f541035 Author: Jan Zill Date: Sun May 25 08:32:05 2025 +1000 more comments commit 9d12b08fd3dbcbb740d5317d6d91a385c160c437 Author: Jan Zill Date: Sun May 25 07:55:04 2025 +1000 comments commit 6f8958be3a0b869156cb64ab673911585d637d42 Author: Jan Zill Date: Sat Apr 12 05:54:06 2025 +1000 lint commit 5637fb0d84ec9c13887ed40534750e300914f096 Author: Jan Zill Date: Sat Apr 12 05:41:00 2025 +1000 test SOA MC for disagg access commit 6a0987d96590f8502353f43c710527b419d716ae Author: Jan Zill Date: Sat Apr 12 05:31:56 2025 +1000 identifies eet todos commit c6d60c6cbf577fc80f0ac6cc73a62d3fb9ff1073 Author: Jan Zill Date: Sat Apr 12 05:23:30 2025 +1000 comment clean up commit 5105b17cbdcf6bf47141a6c5dcd6145086ceaef3 Merge: 30ddb3e19 de6d0ef21 Author: Jan Zill Date: Mon Apr 7 03:38:42 2025 +1000 Merge remote-tracking branch 'origin/master' into janzill/utilit_based_choices_2025 commit 30ddb3e1970d6648d6b9f05e44a11ace2f02a14b Author: Jan Zill Date: Mon Apr 7 02:44:54 2025 +1000 default compute settings set before, no need to check if none commit 11d86a1f53488587c4d7ce9a4fb37ac871fa4b3c Author: Jan Zill Date: Mon Apr 7 02:34:05 2025 +1000 Adds subcomponent-specific eet setting commit 0f54cec22ee73887efd07e2a7c6f17ba5eb64732 Author: Jan Zill Date: Sun Apr 6 20:07:50 2025 +1000 compute_setting overrides for eet, currently only for interaction_sample to test loc sampling with MC commit f5f365fdd8d24be137c255632794c9180bb0797d Author: Jan Zill Date: Wed Apr 2 20:23:57 2025 +1000 removes notebook folder commit 73b8c32471485f2c2d7d88fd03046cef309ad993 Author: Jan Zill Date: Wed Apr 2 20:14:44 2025 +1000 sign on logsum commit 14441a6281faa8ed684bf731ebf95f834ba6f43f Author: Jan Zill Date: Wed Apr 2 16:26:28 2025 +1000 corr commit e1e9d088d706019f9c09639aded311dc31dae8ac Author: Jan Zill Date: Wed Apr 2 15:48:46 2025 +1000 seeding corr test commit 114b747b2e9615a53c7cc453557f7ab07088372d Author: Jan Zill Date: Wed Apr 2 14:37:12 2025 +1000 some variation checks, dev notebooks, to delete commit c6feaefa22ce60dcf4d9557066aa095e61a1a4a5 Author: Jan Zill Date: Sat Mar 29 18:17:34 2025 +1000 log eet setting on start up commit 75bb84d9423134ee5781bd61433db10793e0d035 Author: Jan Zill Date: Sat Mar 29 17:56:50 2025 +1000 more merge fixes, example runs through commit 08d013ab1fcfbdb944062b657e0b68fc38d711aa Author: Jan Zill Date: Sat Mar 29 17:49:10 2025 +1000 more merge bugs commit 792d183eb76010803c477e5c61fead77c71205f2 Author: Jan Zill Date: Sat Mar 29 17:24:25 2025 +1000 merge bug commit b86093fad67369f167f3b0d38073815e101ece67 Merge: 9a1739556 bf64b7beb Author: Jan Zill Date: Sat Mar 29 17:00:53 2025 +1000 Merge branch 'master' into janzill/utilit_based_choices_2025 commit 9a1739556f81847a2057a0e1ec8561573df97c93 Merge: 6b0b4e93e ae7fa438e Author: janzill Date: Tue Jan 31 15:41:45 2023 +1000 resolves merge conflicts with main v1.2 commit 6b0b4e93e575bd5d31dd6930b5b526c74fe91be5 Author: Jan Zill Date: Mon Jun 27 11:13:44 2022 +1000 memory logging commit c0d1ca17ab4fa8d5bd643a97687e21a8af8835dd Author: Jan Zill Date: Fri Jun 24 16:04:36 2022 +1000 gumbel by hand by inverse cdf commit 3973bcde0f590408ab4fdc3587e1b04ebb758ac9 Author: Jan Zill Date: Fri Jun 24 10:34:24 2022 +1000 clean up commit 6451abd8ac1dcc10158d0aa045a84359d1a71f6c Author: Jan Zill Date: Fri Jun 24 10:24:33 2022 +1000 use numpy's gumbel to draw from ev1 commit 313bcd0926bd955a48aa59d046561762812fb60b Author: janzill Date: Tue Jun 21 15:16:51 2022 +1000 remove scale and location params from ev1 quartile fct commit 26a7090218577a95c3b8afd6d220517069fb6372 Author: janzill Date: Tue Jun 21 13:59:47 2022 +1000 interaction sample dev commit d6f4f97531475c472642fb627b80345efec271a4 Author: Jan Zill Date: Tue Jun 21 07:29:51 2022 +1000 remove comment commit 7df4c8ddb4bee084c32a1f0dfc17ad9b949cc1ba Author: Jan Zill Date: Mon Jun 20 23:36:17 2022 +1000 to numpy once commit 7a66a67ded39e284de7fcecc7d6b4f436fff2bb0 Author: Jan Zill Date: Mon Jun 20 23:10:00 2022 +1000 set index commit c6d75d126735ab4e7fb0291def7c3172c100ff2b Author: Jan Zill Date: Mon Jun 20 23:03:21 2022 +1000 delete after last ref, not before commit 78630be674d9b382646f89df2d9045204585e0a3 Author: Jan Zill Date: Mon Jun 20 23:01:39 2022 +1000 spelling commit 2568b72148f9f2c4be4d5bfe61ea45ce4ccdcff8 Author: Jan Zill Date: Mon Jun 20 23:00:59 2022 +1000 memory work commit e0555d439fe2c75265d16f3d78db440130162ec9 Author: Jan Zill Date: Mon Jun 20 22:43:13 2022 +1000 interaction_sample memory saving implementation commit 47601e3bc97e043deb8a172be3c5fd6d520e855d Author: Jan Zill Date: Mon Jun 20 20:18:40 2022 +1000 bug fix commit 2ec41b18f8e8f9d7de378ed1dc5a59f948cd046c Author: Jan Zill Date: Mon Jun 20 20:00:53 2022 +1000 mem tracing in interaction_sample commit 4c6a524e1eacccbc6327c1cc10f3566d6a31ac0d Author: Jan Zill Date: Mon Jun 20 19:47:07 2022 +1000 custom chooser commit dbc809a4ffda797f10bf8b1b377f70ff2a27a4a7 Author: Jan Zill Date: Mon Jun 20 18:05:10 2022 +1000 add utility based choice option for transit virtual pathbuilder commit b4495932ff7122faab31b7f1c11458a2ade7c053 Author: Jan Zill Date: Mon Jun 20 18:02:29 2022 +1000 bug fix commit f6d81d60d1d3035a9f993c76bd8e5af9dd81e2b5 Author: Jan Zill Date: Mon Jun 20 17:57:00 2022 +1000 bug fix commit f0143e99f3bebbe6f29e6e5a07eb37f4df1eb497 Author: Jan Zill Date: Mon Jun 20 17:54:36 2022 +1000 remove false comparison commit 7a6cc29ee70a06aa752e490418401ab6e895a285 Author: Jan Zill Date: Mon Jun 20 17:38:34 2022 +1000 need probs in interaction_sample - try it the quick way commit 4f825ae9c4cbb6980e7c8632030fe9a786554780 Author: Jan Zill Date: Mon Jun 20 17:22:29 2022 +1000 style fixes commit 417e90feb6b95eda77338334455299ead1ebc8e3 Author: Jan Zill Date: Mon Jun 20 17:17:44 2022 +1000 wrong method in cdap commit f0cc563bdb93bd5ca42cf5005c12474a9f952276 Author: Jan Zill Date: Mon Jun 20 17:16:05 2022 +1000 interaction_sample util based w/p probs commit cb807d13edf11ca199f864f8a6481eabaa336862 Author: Jan Zill Date: Mon Jun 20 16:35:49 2022 +1000 remove usage of choose_individual_max_utility commit eea84ca079c1acef4a521372717816b42c7ccba7 Author: Jan Zill Date: Mon Jun 20 16:31:14 2022 +1000 remove usage of choose_individual_max_utility commit 8ac91f19a056c478c7c380423ecffd342c17bc76 Author: Jan Zill Date: Mon Jun 20 16:28:39 2022 +1000 remove usage of choose_individual_max_utility commit 0387c356e604aa75c388a8156bb3b3f9704159cf Author: Jan Zill Date: Mon Jun 20 16:27:33 2022 +1000 remove usage of choose_individual_max_utility commit 6b38f128566027460966ca082c64592f9da46e0f Author: Jan Zill Date: Mon Jun 20 16:26:48 2022 +1000 remove usage of choose_individual_max_utility commit affe2b6dda4c12ec231b67fcca4b27d12d30a078 Author: Jan Zill Date: Mon Jun 20 16:21:32 2022 +1000 remove usage of choose_individual_max_utility commit 17792f5edb81eb0eb64b3f866b99015b3a1b1f2b Author: Jan Zill Date: Mon Jun 20 16:13:49 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit b48dd71bbf9924351dd27659eec925bd3e4c138b Author: Jan Zill Date: Mon Jun 20 16:09:57 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit 4b5cad2681a3ef6a968bfcf0957961d575d8b6f7 Author: Jan Zill Date: Mon Jun 20 16:02:52 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit f9b1e2e09f6d6094aeaf02eb21009afa57ade4e6 Author: Jan Zill Date: Mon Jun 20 15:59:30 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit 043a0bdf1b29cd4c46db9b40e9a30b5b7bb7a84d Author: Jan Zill Date: Mon Jun 20 14:53:12 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit f890799f8ae869acec3b82e18bd3737e6f3142f2 Author: Jan Zill Date: Mon Jun 20 14:36:24 2022 +1000 fake rand tracing commit d14fac389e298e4334fdecd41a14b7642856fc01 Author: Jan Zill Date: Mon Jun 20 14:34:40 2022 +1000 memory saving work - delete dfs wherever possible commit c26c80c08985b547bd8adb8c89473c7218398470 Author: Jan Zill Date: Sun Jun 19 16:58:25 2022 +1000 no fru for probabilitic lookup tables commit 087ec0ebfcd25c4dc4425b87161993dcb2deb638 Author: Jan Zill Date: Sun Jun 19 16:20:34 2022 +1000 use fru in models that use logit.make_choices w/o simulate wrapper commit 8c167658bfc7cc9650271c5b1bb86a5a6f83c2d9 Author: Jan Zill Date: Sun Jun 19 15:56:29 2022 +1000 make choice indexing for nested logit consistent with mnl and cumsum MC commit 61a314f84ffc66600f7f50ecbafe1d6b1a5630ca Author: Jan Zill Date: Sun Jun 19 15:30:07 2022 +1000 move reporting in make choices commit 19d1222b07f962680ce5838b0350a4744d9516c9 Author: Jan Zill Date: Sun Jun 19 15:22:54 2022 +1000 remove debug print statement commit 9685aca3ef75019d4ee75acdbf3383bc4b0e9397 Author: Jan Zill Date: Sun Jun 19 15:06:50 2022 +1000 make place of call consistent commit 8c1d1ed2b2d71c0e3673b115d121df04157ed6a1 Author: Jan Zill Date: Sun Jun 19 14:30:45 2022 +1000 pass arguments in commit 7fbba93275cca185207023ea465a3ebb7caa109a Author: Jan Zill Date: Sun Jun 19 14:03:39 2022 +1000 fies frozen_rand setting call commit bbce6aa028ca1f6cdfbaaea7312fc9bd5ad76ce7 Author: Jan Zill Date: Sun Jun 19 13:52:33 2022 +1000 nested index instead of name commit 005b1311daa1ccf3bbdae6195d5d4d33db4bfe26 Author: Jan Zill Date: Fri Jun 17 10:29:01 2022 +1000 adds interaction_sample at the module level, not individual model commit 00208e7bd3bccb20cec71c9df9cd7f1bb91f3a9b Author: Jan Zill Date: Fri Jun 17 10:24:49 2022 +1000 adds interaction_sample at the module level, not individual model commit e3e4326bdc8c86e426bb8e67a01a3b656e375f68 Author: Jan Zill Date: Fri Jun 17 09:14:45 2022 +1000 interaction sample frozen rand indiv util commit 64c4e6e2c31a3731734487a6b79eba5e114be6e8 Author: Jan Zill Date: Fri Jun 17 08:57:18 2022 +1000 fixes alternative index as chosen value commit 36789a3a2c32f790f3cfafd7a22c2ce155138e56 Author: Jan Zill Date: Thu Jun 16 18:54:04 2022 +1000 interaction_sample dev work commit 9950f846fe56926df8cf9fd167155e38fb104678 Author: Jan Zill Date: Thu Jun 16 13:28:45 2022 +1000 interaction_sample dev work commit e65635d1fb6c033b551e37fadc058142dd3c815b Author: Jan Zill Date: Thu Jun 16 12:55:01 2022 +1000 interaction sample notebook commit 9ac51c91c1438f183daa0736e451efed782416a0 Author: Jan Zill Date: Wed Jun 15 20:22:41 2022 +1000 usage of interaction_sample_simulate commit f3e16107d070ba2c787308eab2a2a42baafd2dc6 Author: Jan Zill Date: Wed Jun 15 19:41:11 2022 +1000 add setting to interaction_simulate call commit 1d4ca9a20ae3c25dbc25c6be76e83ec0cafb86e3 Author: Jan Zill Date: Wed Jun 15 19:39:22 2022 +1000 froxen rand ind util in interaction simulate commit 1a786f26ac8bfcae0855a3a64c44f2bb47cde81c Author: Jan Zill Date: Wed Jun 15 19:33:05 2022 +1000 implement flag in settings, apply to all simple simulate models commit f725ca69093e52d25cfe3d98d0bbf7f5b477af64 Author: Jan Zill Date: Wed Jun 15 19:05:13 2022 +1000 pass through frozen_impl flag commit 0c6f4c8eb5a81505469dab3b6d531a4361682054 Author: Jan Zill Date: Wed Jun 15 19:03:06 2022 +1000 nested now with alternative numbers, not names, change structure to be more consistent with current impl commit 7694563d6574c78920919a1bb99b26addee5cefb Author: Jan Zill Date: Wed Jun 15 15:48:51 2022 +1000 simple siumlate mnl frozen rand ind util commit 921983fb32d8624bffb08615dcd75f70c0ff33c3 Author: Jan Zill Date: Tue May 17 10:53:07 2022 +1000 adds more convergence analysis commit 8668383a489973a91af2c1c5e5043891f05b1409 Author: Jan Zill Date: Tue May 17 00:26:03 2022 +1000 nicer comparison commit 75c16a28d4a9088ac810835efae880b3919ee879 Author: Jan Zill Date: Mon May 16 21:26:02 2022 +1000 validation commit 3700a9e76decb7d4c0582ddf7cba759081a2edda Author: Jan Zill Date: Mon May 16 17:43:32 2022 +1000 some validation metrics commit 42901bf2e7a331529ccad5420e996fbb4c676617 Author: Jan Zill Date: Mon May 16 16:15:51 2022 +1000 adds goodness of fit test and loop over all trips commit 7bfb80a80839a6df6326302a0ed09fd25e223713 Author: Jan Zill Date: Sun May 15 22:32:41 2022 +1000 adds comment regarding potential random choice runtime improvement commit e821b57b9dd6e9cd9596a3c3453cb96b5129482b Author: Jan Zill Date: Tue May 10 14:12:29 2022 +1000 nest scaling comment commit fba872882d5221fc21775f8e6a1cc0a90e59c9b3 Author: Jan Zill Date: Tue May 10 13:10:43 2022 +1000 nest scales in asim seem to be all between 0 and 1, so not like in larch where they are non-increasing - re-adjust util calc. should really document this somewhere commit 4d0d8232aa5ed9b97340c6f683163140e4f19272 Author: Jan Zill Date: Tue May 10 01:10:24 2022 +1000 inspecting a couple of test cases commit 4ee74ea7b9a291ca3ab413f4644698d423f99d2c Author: Jan Zill Date: Tue May 10 00:40:11 2022 +1000 clean up commit e02b46a8c93451edab1c3d5c44bdb6b5258b38e8 Author: Jan Zill Date: Tue May 10 00:35:23 2022 +1000 compare to larch probs commit ebf64c6a238aee78283d4816ab28bf00d27bfc13 Author: Jan Zill Date: Mon May 9 17:23:50 2022 +1000 larch notebook to calculate probs a different way commit 798439208bf923919432f28bf5ae0f59e19412ec Author: Jan Zill Date: Sun May 8 22:45:27 2022 +1000 removes trace in runtime tests commit 6c17d89bace65edff58d5426cbc34a6c8128518f Author: Jan Zill Date: Sun May 8 22:26:14 2022 +1000 caching nest level names and alternatives commit 9f4d2622b09f52e1cf33ac2389a5a04cd8a59e1f Author: Jan Zill Date: Sun May 8 21:32:31 2022 +1000 check commit c95f68df5c08ffd04916fe0ea82c4f52904838f3 Author: Jan Zill Date: Sun May 8 21:15:41 2022 +1000 fixes bug in choice for tree commit 1286183cd2f34dda168baeaac2a6f84d25bc3ed6 Author: Jan Zill Date: Fri May 6 16:57:43 2022 +1000 output clean up commit 932d5adc13776a1f2d7a44381ac46cbe184fa529 Author: Jan Zill Date: Fri May 6 16:41:13 2022 +1000 scaling in recursive nest utilities commit 7c21407cfd2d667a3bf45f7c3f5ea5a830ca3750 Author: Jan Zill Date: Fri May 6 13:27:26 2022 +1000 adds nested utility scale commit b60702b438bc8612eda6a124b61af7d9569f1cca Author: Jan Zill Date: Fri May 6 12:21:50 2022 +1000 clean up commit 4883c3342bcc0766fb4ea9f43b712b86e0ac5044 Author: Jan Zill Date: Fri May 6 12:13:41 2022 +1000 recursive utility calculation fix commit f2cea72263cb23473544ebeedeb51c7be1060f2a Author: Jan Zill Date: Fri May 6 11:59:01 2022 +1000 two level still off commit fd5ca14771973a4929d4116be0c752f98e7b2214 Author: Jan Zill Date: Thu May 5 21:18:50 2022 +1000 fixes scale of nest utilities commit ab8a212977ba886a0a7041f425a3c35580b911b7 Author: Jan Zill Date: Thu May 5 14:11:42 2022 +1000 probs validation clean up commit a31d97f94e9698a37a032b463884f77e3d5cceac Author: Jan Zill Date: Thu May 5 14:08:14 2022 +1000 probs trip set up commit 5e8bf1648342458fcb903c9f0192cec1855543f5 Author: Jan Zill Date: Thu May 5 12:56:34 2022 +1000 towards validation of probs for single trip commit 5a7ad3c76604846057a897ff78c3b7983738776d Author: Jan Zill Date: Wed May 4 23:41:58 2022 +1000 pull out cruft commit 83a86f5575f79922b9fafea6715fc0d342277846 Author: Jan Zill Date: Wed May 4 23:01:29 2022 +1000 scale of leaf utility commit 5dc1a44ed9303e3269207f128a08842954ae49e1 Author: Jan Zill Date: Wed May 4 22:11:12 2022 +1000 baby steps towards validation commit c6b609afd6ffc881964ba560d66b2a12c8248d0f Author: Jan Zill Date: Wed May 4 21:43:07 2022 +1000 prototype simple_simulate commit a4a76309fdbb0c1fd0ae7647a806e1db5724cbff Author: Jan Zill Date: Wed May 4 20:53:30 2022 +1000 adds choice based on walking nest levels commit c973be8130388a7bf061f4d58c079518bcfdcefc Author: Jan Zill Date: Wed May 4 15:52:43 2022 +1000 prototype work - return leaf and node (exp max) utilities, nest spec commit ec8ae759c270add73538a5d79f233b8613a5efe2 Author: Jan Zill Date: Tue May 3 21:10:55 2022 +1000 notebook dev, adds nest coefficient in logit.py --- .../abm/models/disaggregate_accessibility.py | 13 +- .../abm/models/joint_tour_participation.py | 64 ++- activitysim/abm/models/location_choice.py | 13 + .../abm/models/parking_location_choice.py | 7 + .../abm/models/trip_departure_choice.py | 60 +-- activitysim/abm/models/trip_destination.py | 9 +- .../abm/models/trip_scheduling_choice.py | 11 +- activitysim/abm/models/util/cdap.py | 29 +- activitysim/abm/tables/shadow_pricing.py | 1 + activitysim/core/chunk.py | 1 + activitysim/core/configuration/base.py | 12 + activitysim/core/configuration/top.py | 14 +- activitysim/core/interaction_sample.py | 369 ++++++++++++++---- .../core/interaction_sample_simulate.py | 209 ++++++---- activitysim/core/interaction_simulate.py | 53 ++- activitysim/core/logit.py | 251 +++++++++++- activitysim/core/pathbuilder.py | 59 ++- activitysim/core/random.py | 84 +++- activitysim/core/simulate.py | 289 +++++++++----- activitysim/core/test/test_logit.py | 67 +++- 20 files changed, 1281 insertions(+), 334 deletions(-) diff --git a/activitysim/abm/models/disaggregate_accessibility.py b/activitysim/abm/models/disaggregate_accessibility.py index aa2703e19c..e5a66916bd 100644 --- a/activitysim/abm/models/disaggregate_accessibility.py +++ b/activitysim/abm/models/disaggregate_accessibility.py @@ -15,7 +15,11 @@ from activitysim.abm.models.util import tour_destination from activitysim.abm.tables import shadow_pricing from activitysim.core import estimation, los, tracing, util, workflow -from activitysim.core.configuration.base import PreprocessorSettings, PydanticReadable +from activitysim.core.configuration.base import ( + ComputeSettings, + PreprocessorSettings, + PydanticReadable, +) from activitysim.core.configuration.logit import TourLocationComponentSettings from activitysim.core.expressions import assign_columns @@ -184,6 +188,8 @@ class DisaggregateAccessibilitySettings(PydanticReadable, extra="forbid"): If not supplied or None, will default to the chunk size in the location choice model settings. """ + compute_settings: ComputeSettings | None = None + def read_disaggregate_accessibility_yaml( state: workflow.State, file_name @@ -783,6 +789,11 @@ def get_disaggregate_logsums( if disagg_model_settings.explicit_chunk is not None: model_settings.explicit_chunk = disagg_model_settings.explicit_chunk + # Can set compute settings for disaggregate accessibility + # Otherwise this will be set to whatever is in the location model settings + if disagg_model_settings.compute_settings is not None: + model_settings.compute_settings = disagg_model_settings.compute_settings + # Include the suffix tags to pass onto downstream logsum models (e.g., tour mode choice) if model_settings.LOGSUM_SETTINGS: suffixes = util.concat_suffix_dict(disagg_model_settings.suffixes) diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 778ae0f449..1c8f3af9d0 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -20,8 +20,8 @@ ) from activitysim.core.configuration.base import ComputeSettings, PreprocessorSettings from activitysim.core.configuration.logit import LogitComponentSettings -from activitysim.core.util import assign_in_place, reindex from activitysim.core.exceptions import InvalidTravelError +from activitysim.core.util import assign_in_place, reindex logger = logging.getLogger(__name__) @@ -127,7 +127,7 @@ def get_tour_satisfaction(candidates, participate): def participants_chooser( state: workflow.State, - probs: pd.DataFrame, + probs_or_utils: pd.DataFrame, choosers: pd.DataFrame, spec: pd.DataFrame, trace_label: str, @@ -147,9 +147,10 @@ def participants_chooser( Parameters ---------- - probs : pandas.DataFrame + probs_or_utils : pandas.DataFrame Rows for choosers and columns for the alternatives from which they - are choosing. Values are expected to be valid probabilities across + are choosing. If running with explicit_error_terms, these are utilities. + Otherwise, values are expected to be valid probabilities across each row, e.g. they should sum to 1. choosers : pandas.dataframe simple_simulate choosers df @@ -166,7 +167,7 @@ def participants_chooser( """ - assert probs.index.equals(choosers.index) + assert probs_or_utils.index.equals(choosers.index) # choice is boolean (participate or not) model_settings = JointTourParticipationSettings.read_settings_file( @@ -202,7 +203,7 @@ def participants_chooser( "%s max iterations exceeded (%s).", trace_label, MAX_ITERATIONS ) diagnostic_cols = ["tour_id", "household_id", "composition", "adult"] - unsatisfied_candidates = candidates[diagnostic_cols].join(probs) + unsatisfied_candidates = candidates[diagnostic_cols].join(probs_or_utils) state.tracing.write_csv( unsatisfied_candidates, file_name="%s.UNSATISFIED" % trace_label, @@ -215,9 +216,31 @@ def participants_chooser( f"Forcing joint tour participation for {num_tours_remaining} tours." ) # anybody with probability > 0 is forced to join the joint tour - probs[choice_col] = np.where(probs[choice_col] > 0, 1, 0) - non_choice_col = [col for col in probs.columns if col != choice_col][0] - probs[non_choice_col] = 1 - probs[choice_col] + if state.settings.use_explicit_error_terms: + # need "is valid choice" such that we certainly choose those with non-zero values, + # and do not choose others. Let's use 3.0 as large value here. + probs_or_utils[choice_col] = np.where( + probs_or_utils[choice_col] > logit.UTIL_MIN, + 3.0, + logit.UTIL_UNAVAILABLE, + ) + non_choice_col = [ + col for col in probs_or_utils.columns if col != choice_col + ][0] + probs_or_utils[non_choice_col] = np.where( + probs_or_utils[choice_col] <= logit.UTIL_MIN, + 3.0, + logit.UTIL_UNAVAILABLE, + ) + else: + probs_or_utils[choice_col] = np.where( + probs_or_utils[choice_col] > 0, 1, 0 + ) + non_choice_col = [ + col for col in probs_or_utils.columns if col != choice_col + ][0] + probs_or_utils[non_choice_col] = 1 - probs_or_utils[choice_col] + if iter > MAX_ITERATIONS + 1: raise InvalidTravelError( f"{num_tours_remaining} tours could not be satisfied even with forcing participation" @@ -227,8 +250,13 @@ def participants_chooser( f"{num_tours_remaining} tours could not be satisfied after {iter} iterations" ) - choices, rands = logit.make_choices( - state, probs, trace_label=trace_label, trace_choosers=choosers + choice_function = ( + logit.make_choices_utility_based + if state.settings.use_explicit_error_terms + else logit.make_choices + ) + choices, rands = choice_function( + state, probs_or_utils, trace_label=trace_label, trace_choosers=choosers ) participate = choices == PARTICIPATE_CHOICE @@ -252,7 +280,7 @@ def participants_chooser( rands_list.append(rands[satisfied]) # remove candidates of satisfied tours - probs = probs[~satisfied] + probs_or_utils = probs_or_utils[~satisfied] candidates = candidates[~satisfied] logger.debug( @@ -401,6 +429,16 @@ def joint_tour_participation( if i not in model_settings.compute_settings.protect_columns: model_settings.compute_settings.protect_columns.append(i) + # TODO EET: this is related to the difference in nested logit and logit choice as per comment in + # make_choices_utility_based. As soon as alt_order_array is removed from arguments to + # make_choices_explicit_error_term_nl this guard can be removed + if state.settings.use_explicit_error_terms: + assert ( + nest_spec is None + ), "Nested logit model custom chooser for EET requires name_mapping, currently not implemented in jtp" + + custom_chooser = participants_chooser + choices = simulate.simple_simulate_by_chunk_id( state, choosers=candidates, @@ -409,7 +447,7 @@ def joint_tour_participation( locals_d=constants, trace_label=trace_label, trace_choice_name="participation", - custom_chooser=participants_chooser, + custom_chooser=custom_chooser, estimator=estimator, compute_settings=model_settings.compute_settings, ) diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index a8b8fd8a18..360ebc6b1f 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -17,6 +17,7 @@ ) from activitysim.core.interaction_sample import interaction_sample from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.logit import AltsContext from activitysim.core.util import reindex from activitysim.core.exceptions import DuplicateWorkflowTableError @@ -603,6 +604,7 @@ def run_location_simulate( chunk_tag, trace_label, skip_choice=False, + alts_context: AltsContext | None = None, ): """ run location model on location_sample annotated with mode_choice logsum @@ -713,6 +715,7 @@ def run_location_simulate( compute_settings=model_settings.compute_settings.subcomponent_settings( "simulate" ), + alts_context=alts_context, ) if not want_logsums: @@ -789,6 +792,11 @@ def run_location_choice( if choosers.shape[0] == 0: logger.info(f"{trace_label} skipping segment {segment_name}: no choosers") continue + # using land use rather than size terms in case something goes 0 base -> nonzero project, double + # check if that would be in dest_size_terms as a zero + alts_context = AltsContext.from_series(dest_size_terms.index) # index zone_id, not ALT_DEST_COL_NAME + # assumes that dest_size_terms will always contain zeros for non-attractive zones, i.e. it will have the + # same length as land_use # - location_sample location_sample_df = run_location_sample( @@ -842,6 +850,7 @@ def run_location_choice( trace_label, "simulate.%s" % segment_name ), skip_choice=skip_choice, + alts_context=alts_context, ) if estimator: @@ -1020,6 +1029,10 @@ def iterate_location_choice( ) = None # initialize to None, will be populated in first iteration for iteration in range(1, max_iterations + 1): + # Force reset the setting at the start of each shadow price iteration for consistency + logger.info("Resetting random number seeds for iteration {}".format(iteration)) + state.get_rn_generator().end_step(trace_label) + state.get_rn_generator().begin_step(trace_label) persons_merged_df_ = persons_merged_df.copy() if spc.use_shadow_pricing and iteration > 1: diff --git a/activitysim/abm/models/parking_location_choice.py b/activitysim/abm/models/parking_location_choice.py index 32f3aabee2..5df4f3f687 100644 --- a/activitysim/abm/models/parking_location_choice.py +++ b/activitysim/abm/models/parking_location_choice.py @@ -21,6 +21,7 @@ from activitysim.core.configuration.base import PreprocessorSettings from activitysim.core.configuration.logit import LogitComponentSettings from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.logit import AltsContext from activitysim.core.tracing import print_elapsed_time from activitysim.core.util import assign_in_place, drop_unused_columns from activitysim.core.exceptions import DuplicateWorkflowTableError @@ -112,6 +113,7 @@ def parking_destination_simulate( chunk_size, trace_hh_id, trace_label, + alts_context: AltsContext | None = None, ): """ Chose destination from destination_sample (with od_logsum and dp_logsum columns added) @@ -150,6 +152,7 @@ def parking_destination_simulate( trace_label=trace_label, trace_choice_name="parking_loc", explicit_chunk_size=model_settings.explicit_chunk, + alts_context=alts_context, ) # drop any failed zero_prob destinations @@ -211,6 +214,9 @@ def choose_parking_location( ) destination_sample.index = np.repeat(trips.index.values, len(alternatives)) destination_sample.index.name = trips.index.name + # using destination_sample would also be right because destination_sample isn't a sample here, + # but that could change + alts_context = AltsContext.from_series(alternatives[alt_dest_col_name]) destinations = parking_destination_simulate( state, @@ -223,6 +229,7 @@ def choose_parking_location( chunk_size=chunk_size, trace_hh_id=trace_hh_id, trace_label=trace_label, + alts_context=alts_context ) if want_sample_table: diff --git a/activitysim/abm/models/trip_departure_choice.py b/activitysim/abm/models/trip_departure_choice.py index 7b34f8e742..a0bf95e5a3 100644 --- a/activitysim/abm/models/trip_departure_choice.py +++ b/activitysim/abm/models/trip_departure_choice.py @@ -24,10 +24,10 @@ PreprocessorSettings, PydanticCompute, ) +from activitysim.core.exceptions import SegmentedSpecificationError from activitysim.core.skim_dataset import SkimDataset from activitysim.core.skim_dictionary import SkimDict from activitysim.core.util import reindex -from activitysim.core.exceptions import SegmentedSpecificationError logger = logging.getLogger(__name__) @@ -351,37 +351,51 @@ def choose_tour_leg_pattern( column_labels=["alternative", "utility"], ) - # convert to probabilities (utilities exponentiated and normalized to probs) - # probs is same shape as utilities, one row per chooser and one column for alternative - probs = logit.utils_to_probs( - state, utilities_df, trace_label=trace_label, trace_choosers=trip_segment - ) + if state.settings.use_explicit_error_terms: + utilities_df = logit.validate_utils( + state, utilities_df, trace_label=trace_label, trace_choosers=trip_segment + ) + # make choices + # positions is series with the chosen alternative represented as a column index in probs + # which is an integer between zero and num alternatives in the alternative sample + positions, rands = logit.make_choices_utility_based( + state, utilities_df, trace_label=trace_label, trace_choosers=trip_segment + ) + + del utilities_df + chunk_sizer.log_df(trace_label, "utilities_df", None) + else: + # convert to probabilities (utilities exponentiated and normalized to probs) + # probs is same shape as utilities, one row per chooser and one column for alternative + probs = logit.utils_to_probs( + state, utilities_df, trace_label=trace_label, trace_choosers=trip_segment + ) - chunk_sizer.log_df(trace_label, "probs", probs) + chunk_sizer.log_df(trace_label, "probs", probs) - del utilities_df - chunk_sizer.log_df(trace_label, "utilities_df", None) + del utilities_df + chunk_sizer.log_df(trace_label, "utilities_df", None) - if have_trace_targets: - state.tracing.trace_df( - probs, - tracing.extend_trace_label(trace_label, "probs"), - column_labels=["alternative", "probability"], + if have_trace_targets: + state.tracing.trace_df( + probs, + tracing.extend_trace_label(trace_label, "probs"), + column_labels=["alternative", "probability"], + ) + + # make choices + # positions is series with the chosen alternative represented as a column index in probs + # which is an integer between zero and num alternatives in the alternative sample + positions, rands = logit.make_choices( + state, probs, trace_label=trace_label, trace_choosers=trip_segment ) - # make choices - # positions is series with the chosen alternative represented as a column index in probs - # which is an integer between zero and num alternatives in the alternative sample - positions, rands = logit.make_choices( - state, probs, trace_label=trace_label, trace_choosers=trip_segment - ) + del probs + chunk_sizer.log_df(trace_label, "probs", None) chunk_sizer.log_df(trace_label, "positions", positions) chunk_sizer.log_df(trace_label, "rands", rands) - del probs - chunk_sizer.log_df(trace_label, "probs", None) - # shouldn't have chosen any of the dummy pad utilities assert positions.max() < max_sample_count diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 853cfc35e9..3ad35470cd 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -32,6 +32,7 @@ from activitysim.core.configuration.logit import LocationComponentSettings from activitysim.core.interaction_sample import interaction_sample from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.logit import AltsContext from activitysim.core.skim_dictionary import DataFrameMatrix from activitysim.core.tracing import print_elapsed_time from activitysim.core.util import assign_in_place, reindex @@ -950,6 +951,7 @@ def trip_destination_simulate( skim_hotel, estimator, trace_label, + alts_context: AltsContext | None = None, ): """ Chose destination from destination_sample (with od_logsum and dp_logsum columns added) @@ -1036,6 +1038,7 @@ def trip_destination_simulate( trace_choice_name="trip_dest", estimator=estimator, explicit_chunk_size=model_settings.explicit_chunk, + alts_context=alts_context, ) if not want_logsums: @@ -1126,7 +1129,10 @@ def choose_trip_destination( destination_sample["dp_logsum"] = 0.0 t0 = print_elapsed_time("%s.compute_logsums" % trace_label, t0, debug=True) - + alt_dest_col_name = model_settings.ALT_DEST_COL_NAME + alts = alternatives.index + assert alts.name == alt_dest_col_name + alts_context = AltsContext.from_series(alts) destinations = trip_destination_simulate( state, primary_purpose=primary_purpose, @@ -1138,6 +1144,7 @@ def choose_trip_destination( skim_hotel=skim_hotel, estimator=estimator, trace_label=trace_label, + alts_context=alts_context, ) dropped_trips = ~trips.index.isin(destinations.index) diff --git a/activitysim/abm/models/trip_scheduling_choice.py b/activitysim/abm/models/trip_scheduling_choice.py index 094b00843c..254155c333 100644 --- a/activitysim/abm/models/trip_scheduling_choice.py +++ b/activitysim/abm/models/trip_scheduling_choice.py @@ -19,6 +19,7 @@ PydanticReadable, ) from activitysim.core.interaction_sample_simulate import _interaction_sample_simulate +from activitysim.core.logit import AltsContext from activitysim.core.skim_dataset import SkimDataset from activitysim.core.skim_dictionary import SkimDict @@ -275,7 +276,7 @@ def run_trip_scheduling_choice( choosers, chunk_trace_label, chunk_sizer, - ) in chunk.adaptive_chunked_choosers(state, indirect_tours, trace_label): + ) in chunk.adaptive_chunked_choosers(state, indirect_tours, trace_label, explicit_chunk_size=model_settings.explicit_chunk): # Sort the choosers and get the schedule alternatives choosers = choosers.sort_index() schedules = generate_schedule_alternatives(choosers).sort_index() @@ -302,6 +303,7 @@ def run_trip_scheduling_choice( estimator=None, chunk_sizer=chunk_sizer, compute_settings=model_settings.compute_settings, + alts_context= AltsContext(schedules[SCHEDULE_ID].min(), schedules[SCHEDULE_ID].max()), ) assert len(choices.index) == len(choosers.index) @@ -347,6 +349,13 @@ class TripSchedulingChoiceSettings(PydanticReadable, extra="forbid"): compute_settings: ComputeSettings = ComputeSettings() """Compute settings for this component.""" + explicit_chunk: float = 0 + """ + If > 0, use this chunk size instead of adaptive chunking. + If less than 1, use this fraction of the total number of rows. + """ + + @workflow.step def trip_scheduling_choice( diff --git a/activitysim/abm/models/util/cdap.py b/activitysim/abm/models/util/cdap.py index 21f42de827..48ec7a31a1 100644 --- a/activitysim/abm/models/util/cdap.py +++ b/activitysim/abm/models/util/cdap.py @@ -999,11 +999,18 @@ def household_activity_choices( # add joint util to util utils = utils.add(joint_tour_utils) - probs = logit.utils_to_probs(state, utils, trace_label=trace_label) + if state.settings.use_explicit_error_terms: + utils = logit.validate_utils(state, utils, trace_label=trace_label) - # select an activity pattern alternative for each household based on probability - # result is a series indexed on _hh_index_ with the (0 based) index of the column from probs - idx_choices, rands = logit.make_choices(state, probs, trace_label=trace_label) + idx_choices, rands = logit.make_choices_utility_based( + state, utils, trace_label=trace_label + ) + else: + probs = logit.utils_to_probs(state, utils, trace_label=trace_label) + + # select an activity pattern alternative for each household based on probability + # result is a series indexed on _hh_index_ with the (0 based) index of the column from probs + idx_choices, rands = logit.make_choices(state, probs, trace_label=trace_label) # convert choice expressed as index into alternative name from util column label choices = pd.Series(utils.columns[idx_choices].values, index=utils.index) @@ -1021,16 +1028,20 @@ def household_activity_choices( "%s.hhsize%d_utils" % (trace_label, hhsize), column_labels=["expression", "household"], ) - state.tracing.trace_df( - probs, - "%s.hhsize%d_probs" % (trace_label, hhsize), - column_labels=["expression", "household"], - ) + + if not state.settings.use_explicit_error_terms: + state.tracing.trace_df( + probs, + "%s.hhsize%d_probs" % (trace_label, hhsize), + column_labels=["expression", "household"], + ) + state.tracing.trace_df( choices, "%s.hhsize%d_activity_choices" % (trace_label, hhsize), column_labels=["expression", "household"], ) + state.tracing.trace_df( rands, "%s.hhsize%d_rands" % (trace_label, hhsize), columns=[None, "rand"] ) diff --git a/activitysim/abm/tables/shadow_pricing.py b/activitysim/abm/tables/shadow_pricing.py index f993df445c..091aaf7504 100644 --- a/activitysim/abm/tables/shadow_pricing.py +++ b/activitysim/abm/tables/shadow_pricing.py @@ -171,6 +171,7 @@ def __init__( self.num_processes = num_processes self.use_shadow_pricing = bool(state.settings.use_shadow_pricing) + logger.info("Use shadow pricing: %s", self.use_shadow_pricing) self.saved_shadow_price_file_path = ( None # set by read_saved_shadow_prices if loaded ) diff --git a/activitysim/core/chunk.py b/activitysim/core/chunk.py index f0074683d8..019f6abaef 100644 --- a/activitysim/core/chunk.py +++ b/activitysim/core/chunk.py @@ -1244,6 +1244,7 @@ def adaptive_chunked_choosers( chunk_size = math.ceil(num_choosers * explicit_chunk_size) else: chunk_size = math.ceil(explicit_chunk_size / num_processes) + logger.info(f"Running {trace_label} with {chunk_size} choosers") elif chunk_size is None: chunk_size = state.settings.chunk_size diff --git a/activitysim/core/configuration/base.py b/activitysim/core/configuration/base.py index 6936e6d1ad..aad266ff12 100644 --- a/activitysim/core/configuration/base.py +++ b/activitysim/core/configuration/base.py @@ -135,6 +135,10 @@ class ComputeSettings(PydanticBase): Sharrow settings for a component. """ + # Make this more general compute settings and use for explicit error term overrides + # Default None work for sub-components defined in getter below (eet_subcomponent) + use_explicit_error_terms: None | bool | dict[str, bool] = None + sharrow_skip: bool | dict[str, bool] = False """Skip sharrow when evaluating this component. @@ -218,6 +222,13 @@ def should_skip(self, subcomponent: str) -> bool: else: return bool(self.sharrow_skip) + def eet_subcomponent(self, subcomponent: str) -> bool: + """Check for EET overrides for a particular subcomponent.""" + if isinstance(self.use_explicit_error_terms, dict): + return self.use_explicit_error_terms.get(subcomponent, None) + else: + return self.use_explicit_error_terms + @contextmanager def pandas_option_context(self): """Context manager to set pandas options for compute settings.""" @@ -266,6 +277,7 @@ def subcomponent_settings(self, subcomponent: str) -> ComputeSettings: use_numba=self.use_numba, drop_unused_columns=self.drop_unused_columns, protect_columns=self.protect_columns, + use_explicit_error_terms=self.eet_subcomponent(subcomponent), ) diff --git a/activitysim/core/configuration/top.py b/activitysim/core/configuration/top.py index 4055608100..bded7123d9 100644 --- a/activitysim/core/configuration/top.py +++ b/activitysim/core/configuration/top.py @@ -701,6 +701,7 @@ def _check_store_skims_in_shm(self): "memory_profile", "instrument", "sharrow", + "use_explicit_error_terms", ) """ Setting to log on startup. @@ -778,7 +779,18 @@ def _check_store_skims_in_shm(self): """ run checks to validate that YAML settings files are loadable and spec and coefficent csv can be resolved. - should catch many common errors early, including missing required configurations or specified coefficient labels without defined values. + should catch many common errors early, including missing required configurations or specified coefficient labels without defined values. + """ + + use_explicit_error_terms: bool = False + """ + Make choice from random utility model by drawing from distribution of unobserved + part of utility and taking the maximum of total utility. + + Defaults to standard Monte Carlo method, i.e., calculating probabilities and then + drawing a single uniform random number to draw from cumulative probabily. + + .. versionadded:: 1.x """ other_settings: dict[str, Any] = None diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index 52116638ba..e66840778a 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -3,10 +3,10 @@ from __future__ import annotations import logging +import typing import numpy as np import pandas as pd - from activitysim.core import ( chunk, estimation, @@ -17,16 +17,143 @@ util, workflow, ) +from activitysim.core.chunk import ChunkSizer from activitysim.core.configuration.base import ComputeSettings +from activitysim.core.exceptions import SegmentedSpecificationError from activitysim.core.skim_dataset import DatasetWrapper from activitysim.core.skim_dictionary import SkimWrapper -from activitysim.core.exceptions import SegmentedSpecificationError +if typing.TYPE_CHECKING: + from activitysim.core.random import Random logger = logging.getLogger(__name__) DUMP = False +def _poisson_sample_alternatives_inner( + alternative_count: int, + probs: pd.DataFrame, + poisson_inclusion_probs: pd.DataFrame, + rng: Random, + trace_label: str | None, + chunk_sizer:ChunkSizer, +) -> pd.DataFrame: + rands = rng.random_for_df(probs, n=alternative_count) + chunk_sizer.log_df(trace_label, "rands", rands) + sampled_mask = rands < poisson_inclusion_probs + sampled_results = poisson_inclusion_probs.where(sampled_mask) + return sampled_results + + +def make_sample_choices_utility_based( + state: workflow.State, + choosers, + utilities, + alternatives, + sample_size, + alternative_count, + alt_col_name, + allow_zero_probs, + trace_label:str, + chunk_sizer:ChunkSizer, +): + assert isinstance(utilities, pd.DataFrame) + assert utilities.shape == (len(choosers), alternative_count) + + assert isinstance(alternatives, pd.DataFrame) + assert len(alternatives) == alternative_count + + if allow_zero_probs: + zero_probs = ( + utilities.sum(axis=1) <= utilities.shape[1] * logit.UTIL_UNAVAILABLE + ) + if zero_probs.all(): + utils = pd.DataFrame( + columns=[alt_col_name, "rand", "prob", choosers.index.name] + ) + probs = pd.DataFrame(0.0, index=utils.index, columns=utils.columns) + return utils, probs + if zero_probs.any(): + # remove from sample + utilities = utilities[~zero_probs] + choosers = choosers[~zero_probs] + + utils_array = utilities.to_numpy() + chunk_sizer.log_df(trace_label, "utils_array", utils_array) + + probs = logit.utils_to_probs( + state, + utilities, + allow_zero_probs=allow_zero_probs, + trace_label=trace_label, + overflow_protection=not allow_zero_probs, + trace_choosers=choosers, + ) + inclusion_probs, sampled_alternatives = _poisson_sample_alternatives(alternative_count, chunk_sizer, probs, + sample_size, state, trace_label) + + # Stack removes the NaNs (the ones that weren't sampled) + # and gives us a multi-index of (person_id, alt_id) + choices_df = ( + sampled_alternatives.rename_axis("alt_idx", axis=1) + .stack() + .reset_index(name="prob") + .assign(**{alt_col_name: lambda df: alternatives.index.values[df["alt_idx"]]}) + .drop(columns=["alt_idx"]) + ) + + # Here we return the inclusion probabilities i.e. the true probability of being sampled and (ab)use the fact + # that pick_count=1 by definition and ln(1)=0 and recover the standard sample correction term. + # In non-Poisson sampling, we would return the probs of sampling an alternative once + # and the sampling correction factor np.log(df.pick_count/df.prob) is applied to the simulate utilities. + # TODO is it safe change the meaning of df.prob, given it's referenced in expression csvs? + # (but the alternative is to update all the expression CSV for sampling?) + return choices_df, inclusion_probs + + +def _poisson_sample_alternatives(alternative_count, chunk_sizer: ChunkSizer, probs: pd.DataFrame, sample_size, + state: workflow.State, trace_label: str) -> tuple[pd.DataFrame, pd.DataFrame]: + # compute the inclusion probability as the reciprocal of alt never being drawn + # -- these are common, so compute once upfront + exclusion_probs = (1 - probs) ** sample_size + inclusion_probs = 1 - exclusion_probs + + n = 0 + probs_subset = probs + inclusion_probs_subset = inclusion_probs + sampled_alternatives = pd.DataFrame(0.0, index=inclusion_probs.index, columns=inclusion_probs.columns) + while True: + sampled_results_subset = _poisson_sample_alternatives_inner( + alternative_count, probs_subset, inclusion_probs_subset, state.get_rn_generator(), trace_label, chunk_sizer + ) + no_alts_sampled_mask = sampled_results_subset.isna().all(axis=1) + alts_with_sampled_alternatives = sampled_results_subset[~no_alts_sampled_mask] + sampled_alternatives.loc[alts_with_sampled_alternatives.index, :] = alts_with_sampled_alternatives + if no_alts_sampled_mask.any(): + # TODO if this happens in base but the project case is such that something is picked, random numbers won't + # be consistent - we're asserting that this is very rare models where the sample size is not too small + logger.info(f"Poisson sampling of alternatives failed with {n=}, retrying") + # TODO put this behind a debug guard, because it will be slow + logger.info( + f"Sampled size was {sample_size}, poisson method mean expected sample size was {inclusion_probs.sum(axis=1).mean():.1f}, actual sampled mean was {(sampled_alternatives > 0).sum(axis=1).mean():.1f} and highest zero selection prob was {(exclusion_probs).product(axis=1).max():.2g}") + probs_subset = probs[no_alts_sampled_mask] + inclusion_probs_subset = inclusion_probs[no_alts_sampled_mask] + + else: # All alternatives are fine + break + + n += 1 + if n == 10: + choosers_no_alts_sampled = sampled_results_subset[no_alts_sampled_mask] + msg = (f"Poisson choice set sampling failed after 10 attempts for these cases:\n" + f"{choosers_no_alts_sampled}\n{probs_subset}") + raise ValueError(msg) + + chunk_sizer.log_df(trace_label, "sampled_alternatives", sampled_alternatives) + + return inclusion_probs, sampled_alternatives + + def make_sample_choices( state: workflow.State, choosers, @@ -135,7 +262,7 @@ def _interaction_sample( locals_d=None, trace_label=None, zone_layer=None, - chunk_sizer=None, + chunk_sizer: ChunkSizer|None=None, compute_settings: ComputeSettings | None = None, ): """ @@ -191,15 +318,18 @@ def _interaction_sample( choices_df : pandas.DataFrame A DataFrame where index should match the index of the choosers DataFrame - and columns alt_col_name, prob, rand, pick_count + and columns alt_col_name, prob, pick_count + alt_col_name: int + the identifier of the alternatives prob: float the probability of the chosen alternative - rand: float - the rand that did the choosing pick_count : int number of duplicate picks for chooser, alt """ + assert chunk_sizer is not None, "chunk_sizer cannot be None but old nullable signature is preserved" + # TODO it's probably safe to reorder these arguments to make chunk_sizer mandatory since + # _interaction_sample is private? have_trace_targets = state.tracing.has_trace_targets(choosers) trace_ids = None @@ -443,54 +573,55 @@ def _interaction_sample( state.tracing.dump_df(DUMP, utilities, trace_label, "utilities") - # convert to probabilities (utilities exponentiated and normalized to probs) - # probs is same shape as utilities, one row per chooser and one column for alternative - probs = logit.utils_to_probs( - state, - utilities, - allow_zero_probs=allow_zero_probs, - trace_label=trace_label, - trace_choosers=choosers, - overflow_protection=not allow_zero_probs, - ) - chunk_sizer.log_df(trace_label, "probs", probs) - - del utilities - chunk_sizer.log_df(trace_label, "utilities", None) - - if have_trace_targets: - state.tracing.trace_df( - probs, - tracing.extend_trace_label(trace_label, "probs"), - column_labels=["alternative", "probability"], + if compute_settings.use_explicit_error_terms is not None: + use_eet = compute_settings.use_explicit_error_terms + logger.info( + f"Interaction sample model-specific EET overrides for {trace_label}: eet = {use_eet}" ) + else: + use_eet = state.settings.use_explicit_error_terms if sample_size == 0: - # FIXME return full alternative set rather than sample - logger.info( - "Estimation mode for %s using unsampled alternatives" % (trace_label,) - ) + # Return full alternative set rather than sample + logger.info("Using unsampled alternatives for %s" % (trace_label,)) - index_name = probs.index.name + index_name = utilities.index.name choices_df = ( - pd.melt(probs.reset_index(), id_vars=[index_name]) + pd.melt( + utilities.reset_index(), + id_vars=[index_name], + value_name="prob", + var_name=alt_col_name, + ) .sort_values(by=index_name, kind="mergesort") .set_index(index_name) - .rename(columns={"value": "prob"}) - .drop(columns="variable") + .assign(prob=1) + .assign(pick_count=1) ) + chunk_sizer.log_df(trace_label, "choices_df", choices_df) - choices_df["pick_count"] = 1 - choices_df.insert( - 0, alt_col_name, np.tile(alternatives.index.values, len(choosers.index)) - ) + # utilities are numbered 0..n-1 so we need to map back to alt ids + alternative_map = pd.Series(alternatives.index).to_dict() + choices_df[alt_col_name] = choices_df[alt_col_name].map(alternative_map) + + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) return choices_df - else: - choices_df = make_sample_choices( + + if use_eet: + utilities = logit.validate_utils( + state, + utilities, + allow_zero_probs=allow_zero_probs, + trace_label=trace_label, + trace_choosers=choosers, + ) + + choices_df, probs = make_sample_choices_utility_based( state, choosers, - probs, + utilities, alternatives, sample_size, alternative_count, @@ -500,45 +631,77 @@ def _interaction_sample( chunk_sizer=chunk_sizer, ) - chunk_sizer.log_df(trace_label, "choices_df", choices_df) + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) - if estimation.manager.enabled and sample_size > 0: - # we need to ensure chosen alternative is included in the sample - survey_choices = estimation.manager.get_survey_destination_choices( - state, choosers, trace_label - ) - if survey_choices is not None: - assert ( - survey_choices.index == choosers.index - ).all(), "survey_choices and choosers must have the same index" - survey_choices.name = alt_col_name - survey_choices = survey_choices.dropna().astype( - choices_df[alt_col_name].dtype + if estimation.manager.enabled and sample_size > 0: + + choices_df = _ensure_chosen_alts_in_sample( + alt_col_name, + alternatives, + choices_df, + choosers, + probs, + state, + trace_label, ) - # merge all survey choices onto choices_df - probs_df = probs.reset_index().melt( - id_vars=[choosers.index.name], - var_name=alt_col_name, - value_name="prob", + del probs + chunk_sizer.log_df(trace_label, "probs", None) + + else: + # convert to probabilities (utilities exponentiated and normalized to probs) + # probs is same shape as utilities, one row per chooser and one column for alternative + probs = logit.utils_to_probs( + state, + utilities, + allow_zero_probs=allow_zero_probs, + trace_label=trace_label, + trace_choosers=choosers, + overflow_protection=not allow_zero_probs, + ) + chunk_sizer.log_df(trace_label, "probs", probs) + + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) + + if have_trace_targets: + state.tracing.trace_df( + probs, + tracing.extend_trace_label(trace_label, "probs"), + column_labels=["alternative", "probability"], ) - # probs are numbered 0..n-1 so we need to map back to alt ids - zone_map = pd.Series(alternatives.index).to_dict() - probs_df[alt_col_name] = probs_df[alt_col_name].map(zone_map) - - survey_choices = pd.merge( - survey_choices, - probs_df, - on=[choosers.index.name, alt_col_name], - how="left", + + choices_df = make_sample_choices( + state, + choosers, + probs, + alternatives, + sample_size, + alternative_count, + alt_col_name, + allow_zero_probs=allow_zero_probs, + trace_label=trace_label, + chunk_sizer=chunk_sizer, + ) + + chunk_sizer.log_df(trace_label, "choices_df", choices_df) + + if estimation.manager.enabled and sample_size > 0: + choices_df = _ensure_chosen_alts_in_sample( + alt_col_name, + alternatives, + choices_df, + choosers, + probs, + state, + trace_label, ) - survey_choices["rand"] = 0 - survey_choices["prob"].fillna(0, inplace=True) - choices_df = pd.concat([choices_df, survey_choices], ignore_index=True) - choices_df.sort_values(by=[choosers.index.name], inplace=True) - del probs - chunk_sizer.log_df(trace_label, "probs", None) + del probs + chunk_sizer.log_df(trace_label, "probs", None) + + chunk_sizer.log_df(trace_label, "choices_df", choices_df) # pick_count and pick_dup # pick_count is number of duplicate picks @@ -570,8 +733,10 @@ def _interaction_sample( column_labels=["sample_alt", "alternative"], ) - # don't need this after tracing - del choices_df["rand"] + if not state.settings.use_explicit_error_terms: + # don't need this after tracing + del choices_df["rand"] + chunk_sizer.log_df(trace_label, "choices_df", choices_df) # - NARROW @@ -582,6 +747,49 @@ def _interaction_sample( return choices_df +def _ensure_chosen_alts_in_sample( + alt_col_name, + alternatives: pd.DataFrame, + choices_df: pd.DataFrame, + choosers: pd.DataFrame, + probs: pd.DataFrame, + state: workflow.State, + trace_label:str, +) -> pd.DataFrame: + # we need to ensure chosen alternative is included in the sample + survey_choices = estimation.manager.get_survey_destination_choices( + state, choosers, trace_label + ) + if survey_choices is not None: + assert ( + survey_choices.index == choosers.index + ).all(), "survey_choices and choosers must have the same index" + survey_choices.name = alt_col_name + survey_choices = survey_choices.dropna().astype(choices_df[alt_col_name].dtype) + + # merge all survey choices onto choices_df + probs_df = probs.reset_index().melt( + id_vars=[choosers.index.name], + var_name=alt_col_name, + value_name="prob", + ) + # probs are numbered 0..n-1 so we need to map back to alt ids + zone_map = pd.Series(alternatives.index).to_dict() + probs_df[alt_col_name] = probs_df[alt_col_name].map(zone_map) + + survey_choices = pd.merge( + survey_choices, + probs_df, + on=[choosers.index.name, alt_col_name], + how="left", + ) + survey_choices["rand"] = 0 + survey_choices["prob"].fillna(0, inplace=True) + choices_df = pd.concat([choices_df, survey_choices], ignore_index=True) + choices_df.sort_values(by=[choosers.index.name], inplace=True) + return choices_df + + def interaction_sample( state: workflow.State, choosers: pd.DataFrame, @@ -674,7 +882,14 @@ def interaction_sample( assert choosers.index.is_monotonic_increasing # FIXME - legacy logic - not sure this is needed or even correct? - sample_size = min(sample_size, len(alternatives.index)) + if not state.settings.use_explicit_error_terms: + sample_size = min(sample_size, len(alternatives.index)) + # with poisson sampling, definitely don't want to reduce sample size - it's not a sample size but a number + # of theoretical draws. Another options would be to disable sampling if # alts < sample size to ensure + # all are included (but this wouldn't behave well if there were land use changes in the project case which + # switched regimes) + + logger.info(f" --- interaction_sample sample size = {sample_size}") result_list = [] for ( diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index df1c53fa03..2dbd52e871 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -9,8 +9,9 @@ from activitysim.core import chunk, interaction_simulate, logit, tracing, util, workflow from activitysim.core.configuration.base import ComputeSettings -from activitysim.core.simulate import set_skim_wrapper_targets from activitysim.core.exceptions import SegmentedSpecificationError +from activitysim.core.logit import AltsContext +from activitysim.core.simulate import set_skim_wrapper_targets logger = logging.getLogger(__name__) @@ -34,6 +35,7 @@ def _interaction_sample_simulate( *, chunk_sizer: chunk.ChunkSizer, compute_settings: ComputeSettings | None = None, + alts_context: AltsContext | None = None, ): """ Run a MNL simulation in the situation in which alternatives must @@ -220,9 +222,6 @@ def _interaction_sample_simulate( ) chunk_sizer.log_df(trace_label, "interaction_utilities", interaction_utilities) - del interaction_df - chunk_sizer.log_df(trace_label, "interaction_df", None) - if have_trace_targets: state.tracing.trace_interaction_eval_results( trace_eval_results, @@ -258,26 +257,37 @@ def _interaction_sample_simulate( # (we want to insert dummy utilities at the END of the list of alternative utilities) # inserts is a list of the indices at which we want to do the insertions inserts = np.repeat(last_row_offsets, max_sample_count - sample_counts) - + del last_row_offsets del sample_counts chunk_sizer.log_df(trace_label, "sample_counts", None) # insert the zero-prob utilities to pad each alternative set to same size padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999) + padded_alt_nrs = np.insert(interaction_df[choice_column], inserts, -999) chunk_sizer.log_df(trace_label, "padded_utilities", padded_utilities) del inserts - + del interaction_df del interaction_utilities + # TODO EET: chunk_sizer logging could be more precise + chunk_sizer.log_df(trace_label, "interaction_df", None) chunk_sizer.log_df(trace_label, "interaction_utilities", None) # reshape to array with one row per chooser, one column per alternative padded_utilities = padded_utilities.reshape(-1, max_sample_count) + padded_alt_nrs = padded_alt_nrs.reshape(-1, max_sample_count) # convert to a dataframe with one row per chooser and one column per alternative utilities_df = pd.DataFrame(padded_utilities, index=choosers.index) + # alt_nrs_df has columns for each alt in the choice set, with values indicating which alt_id + # they correspond to (as opposed to the 0-n index implied by the column number). + if alts_context is not None: + alt_nrs_df = pd.DataFrame(padded_alt_nrs, index=choosers.index) + else: + alt_nrs_df = None # if we don't provide the number of dense alternatives, assume that we'll use the old approach chunk_sizer.log_df(trace_label, "utilities_df", utilities_df) del padded_utilities + del padded_alt_nrs chunk_sizer.log_df(trace_label, "padded_utilities", None) if have_trace_targets: @@ -287,50 +297,89 @@ def _interaction_sample_simulate( column_labels=["alternative", "utility"], ) - # convert to probabilities (utilities exponentiated and normalized to probs) - # probs is same shape as utilities, one row per chooser and one column for alternative - if want_logsums: - probs, logsums = logit.utils_to_probs( + if state.settings.use_explicit_error_terms: + if want_logsums: + logsums = logit.utils_to_logsums( + utilities_df, allow_zero_probs=allow_zero_probs + ) + chunk_sizer.log_df(trace_label, "logsums", logsums) + + if skip_choice: + return choosers.join(logsums.to_frame("logsums")) + + utilities_df = logit.validate_utils( state, utilities_df, allow_zero_probs=allow_zero_probs, trace_label=trace_label, trace_choosers=choosers, - overflow_protection=not allow_zero_probs, - return_logsums=True, ) - chunk_sizer.log_df(trace_label, "logsums", logsums) - else: - probs = logit.utils_to_probs( - state, - utilities_df, - allow_zero_probs=allow_zero_probs, - trace_label=trace_label, - trace_choosers=choosers, - overflow_protection=not allow_zero_probs, + + if allow_zero_probs: + zero_probs = ( + utilities_df.sum(axis=1) + <= utilities_df.shape[1] * logit.UTIL_UNAVAILABLE + ) + if zero_probs.any(): + # copied from proabability below, fix when that gets fixed + # FIXME this is kind of gnarly, but we force choice of first alt + utilities_df.loc[ + zero_probs, 0 + ] = 3.0 # arbitrary value much larger than UTIL_UNAVAILABLE + + # positions is series with the chosen alternative represented as a column index in utilities_df + # which is an integer between zero and num alternatives in the alternative sample + positions, rands = logit.make_choices_utility_based( + state, utilities_df, trace_label=trace_label, trace_choosers=choosers, alts_context=alts_context, + alt_nrs_df=alt_nrs_df ) - chunk_sizer.log_df(trace_label, "probs", probs) - del utilities_df - chunk_sizer.log_df(trace_label, "utilities_df", None) + del utilities_df + chunk_sizer.log_df(trace_label, "utilities_df", None) + else: + # convert to probabilities (utilities exponentiated and normalized to probs) + # probs is same shape as utilities, one row per chooser and one column for alternative + if want_logsums: + probs, logsums = logit.utils_to_probs( + state, + utilities_df, + allow_zero_probs=allow_zero_probs, + trace_label=trace_label, + trace_choosers=choosers, + overflow_protection=not allow_zero_probs, + return_logsums=True, + ) + chunk_sizer.log_df(trace_label, "logsums", logsums) + else: + probs = logit.utils_to_probs( + state, + utilities_df, + allow_zero_probs=allow_zero_probs, + trace_label=trace_label, + trace_choosers=choosers, + overflow_protection=not allow_zero_probs, + ) + chunk_sizer.log_df(trace_label, "probs", probs) + + del utilities_df + chunk_sizer.log_df(trace_label, "utilities_df", None) - if have_trace_targets: - state.tracing.trace_df( - probs, - tracing.extend_trace_label(trace_label, "probs"), - column_labels=["alternative", "probability"], - ) + if have_trace_targets: + state.tracing.trace_df( + probs, + tracing.extend_trace_label(trace_label, "probs"), + column_labels=["alternative", "probability"], + ) - if allow_zero_probs: - zero_probs = probs.sum(axis=1) == 0 - if zero_probs.any(): - # FIXME this is kind of gnarly, but we force choice of first alt - probs.loc[zero_probs, 0] = 1.0 + if allow_zero_probs: + zero_probs = probs.sum(axis=1) == 0 + if zero_probs.any(): + # FIXME this is kind of gnarly, but we force choice of first alt + probs.loc[zero_probs, 0] = 1.0 - if skip_choice: - return choosers.join(logsums.to_frame("logsums")) + if skip_choice: + return choosers.join(logsums.to_frame("logsums")) - else: # make choices # positions is series with the chosen alternative represented as a column index in probs # which is an integer between zero and num alternatives in the alternative sample @@ -338,59 +387,59 @@ def _interaction_sample_simulate( state, probs, trace_label=trace_label, trace_choosers=choosers ) - chunk_sizer.log_df(trace_label, "positions", positions) - chunk_sizer.log_df(trace_label, "rands", rands) - del probs chunk_sizer.log_df(trace_label, "probs", None) - # shouldn't have chosen any of the dummy pad utilities - assert positions.max() < max_sample_count + chunk_sizer.log_df(trace_label, "positions", positions) + chunk_sizer.log_df(trace_label, "rands", rands) - # need to get from an integer offset into the alternative sample to the alternative index - # that is, we want the index value of the row that is offset by rows into the - # tranche of this choosers alternatives created by cross join of alternatives and choosers + # shouldn't have chosen any of the dummy pad utilities + assert positions.max() < max_sample_count - # resulting pandas Int64Index has one element per chooser row and is in same order as choosers - choices = alternatives[choice_column].take(positions + first_row_offsets) + # need to get from an integer offset into the alternative sample to the alternative index + # that is, we want the index value of the row that is offset by rows into the + # tranche of this choosers alternatives created by cross join of alternatives and choosers - # create a series with index from choosers and the index of the chosen alternative - choices = pd.Series(choices, index=choosers.index) + # resulting pandas Int64Index has one element per chooser row and is in same order as choosers + choices = alternatives[choice_column].take(positions + first_row_offsets) - chunk_sizer.log_df(trace_label, "choices", choices) + # create a series with index from choosers and the index of the chosen alternative + choices = pd.Series(choices, index=choosers.index) - if allow_zero_probs and zero_probs.any() and zero_prob_choice_val is not None: - # FIXME this is kind of gnarly, patch choice for zero_probs - choices.loc[zero_probs] = zero_prob_choice_val + chunk_sizer.log_df(trace_label, "choices", choices) - if have_trace_targets: - state.tracing.trace_df( - choices, - tracing.extend_trace_label(trace_label, "choices"), - columns=[None, trace_choice_name], - ) + if allow_zero_probs and zero_probs.any() and zero_prob_choice_val is not None: + # FIXME this is kind of gnarly, patch choice for zero_probs + choices.loc[zero_probs] = zero_prob_choice_val + + if have_trace_targets: + state.tracing.trace_df( + choices, + tracing.extend_trace_label(trace_label, "choices"), + columns=[None, trace_choice_name], + ) + state.tracing.trace_df( + rands, + tracing.extend_trace_label(trace_label, "rands"), + columns=[None, "rand"], + ) + if want_logsums: state.tracing.trace_df( - rands, - tracing.extend_trace_label(trace_label, "rands"), - columns=[None, "rand"], + logsums, + tracing.extend_trace_label(trace_label, "logsum"), + columns=[None, "logsum"], ) - if want_logsums: - state.tracing.trace_df( - logsums, - tracing.extend_trace_label(trace_label, "logsum"), - columns=[None, "logsum"], - ) - if want_logsums: - choices = choices.to_frame("choice") - choices["logsum"] = logsums + if want_logsums: + choices = choices.to_frame("choice") + choices["logsum"] = logsums - chunk_sizer.log_df(trace_label, "choices", choices) + chunk_sizer.log_df(trace_label, "choices", choices) - # handing this off to our caller - chunk_sizer.log_df(trace_label, "choices", None) + # handing this off to our caller + chunk_sizer.log_df(trace_label, "choices", None) - return choices + return choices def interaction_sample_simulate( @@ -413,6 +462,7 @@ def interaction_sample_simulate( skip_choice=False, explicit_chunk_size=0, *, + alts_context: AltsContext | None = None, compute_settings: ComputeSettings | None = None, ): """ @@ -458,6 +508,12 @@ def interaction_sample_simulate( explicit_chunk_size : float, optional If > 0, specifies the chunk size to use when chunking the interaction simulation. If < 1, specifies the fraction of the total number of choosers. + alts_context: int, optional + The number of alternatives available in the choice set in the absense of sampling. + This is used with EET simulation to ensure consistent random numbers across the whole alternative set + ( as the sampled set may change between base and project). When not provided, + the fallback approach is used which may result in frozen error terms being applied to the wrong alternatives + if the choice set changes. Returns ------- @@ -513,6 +569,7 @@ def interaction_sample_simulate( skip_choice, chunk_sizer=chunk_sizer, compute_settings=compute_settings, + alts_context=alts_context, ) result_list.append(choices) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 0d84241395..ef6bbb1217 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -15,8 +15,8 @@ from activitysim.core import chunk, logit, simulate, timing, tracing, util, workflow from activitysim.core.configuration.base import ComputeSettings -from activitysim.core.fast_eval import fast_eval from activitysim.core.exceptions import SegmentedSpecificationError +from activitysim.core.fast_eval import fast_eval logger = logging.getLogger(__name__) @@ -904,29 +904,42 @@ def _interaction_simulate( state.tracing.dump_df(DUMP, utilities, trace_label, "utilities") - # convert to probabilities (utilities exponentiated and normalized to probs) - # probs is same shape as utilities, one row per chooser and one column for alternative - probs = logit.utils_to_probs( - state, utilities, trace_label=trace_label, trace_choosers=choosers - ) - chunk_sizer.log_df(trace_label, "probs", probs) + if state.settings.use_explicit_error_terms: + utilities = logit.validate_utils( + state, utilities, trace_label=trace_label, trace_choosers=choosers + ) + positions, rands = logit.make_choices_utility_based( + state, utilities, trace_label=trace_label, trace_choosers=choosers + ) - del utilities - chunk_sizer.log_df(trace_label, "utilities", None) + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) - if have_trace_targets: - state.tracing.trace_df( - probs, - tracing.extend_trace_label(trace_label, "probs"), - column_labels=["alternative", "probability"], + else: + # convert to probabilities (utilities exponentiated and normalized to probs) + # probs is same shape as utilities, one row per chooser and one column for alternative + probs = logit.utils_to_probs( + state, utilities, trace_label=trace_label, trace_choosers=choosers + ) + chunk_sizer.log_df(trace_label, "probs", probs) + + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) + + if have_trace_targets: + state.tracing.trace_df( + probs, + tracing.extend_trace_label(trace_label, "probs"), + column_labels=["alternative", "probability"], + ) + + # make choices + # positions is series with the chosen alternative represented as a column index in probs + # which is an integer between zero and num alternatives in the alternative sample + positions, rands = logit.make_choices( + state, probs, trace_label=trace_label, trace_choosers=choosers ) - # make choices - # positions is series with the chosen alternative represented as a column index in probs - # which is an integer between zero and num alternatives in the alternative sample - positions, rands = logit.make_choices( - state, probs, trace_label=trace_label, trace_choosers=choosers - ) chunk_sizer.log_df(trace_label, "positions", positions) chunk_sizer.log_df(trace_label, "rands", rands) diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index 105e18fecc..8ab8ee1cf3 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -4,6 +4,8 @@ import logging import warnings +from dataclasses import dataclass +from typing import Union import numpy as np import pandas as pd @@ -22,6 +24,11 @@ EXP_UTIL_MIN = 1e-300 EXP_UTIL_MAX = np.inf +# TODO-EET: Figure out what type we want UTIL_MIN to be, currently np.float64 +UTIL_MIN = np.log(EXP_UTIL_MIN, dtype=np.float64) +UTIL_UNAVAILABLE = 1000.0 * (UTIL_MIN - 1.0) + + PROB_MIN = 0.0 PROB_MAX = 1.0 @@ -128,6 +135,70 @@ def utils_to_logsums(utils, exponentiated=False, allow_zero_probs=False): return logsums +def validate_utils( + state: workflow.State, + utils, + trace_label=None, + allow_zero_probs=False, + trace_choosers=None, +): + """ + Validate utilities to ensure non-available choices are treated the same in EET and MC. + For EET decisions, no conversion to probabilities is required because choices + are made on the basis of comparing utilities (only differences matter). + However, large negative utility values are used in practice to make choices + unavailable based on probability calculations, which boils down to evaluating + exp(utility). We here use this to define a minimum utility that corresponds + to an unavailable choice. + + Parameters + ---------- + utils : pandas.DataFrame + Rows should be choosers and columns should be alternatives. + + trace_label : str, optional + label for tracing bad utility or probability values + + allow_zero_probs : bool + if True value rows in which all utility alts are UTIL_MIN will be set to + UTIL_UNAVAILABLE. + + trace_choosers : pandas.dataframe + the choosers df (for interaction_simulate) to facilitate the reporting of hh_id + by report_bad_choices because it can't deduce hh_id from the interaction_dataset + which is indexed on index values from alternatives df + + Returns + ------- + utils : pandas.DataFrame + utils with values that would lead to zero probability replaced by UTIL_UNAVAILABLE + + """ + trace_label = tracing.extend_trace_label(trace_label, "validate_utils") + + utils_arr = utils.values + + np.putmask(utils_arr, utils_arr <= UTIL_MIN, UTIL_UNAVAILABLE) + + arr_sum = utils_arr.sum(axis=1) + + if not allow_zero_probs: + zero_probs = arr_sum <= utils_arr.shape[1] * UTIL_UNAVAILABLE + if zero_probs.any(): + report_bad_choices( + state, + zero_probs, + utils, + trace_label=tracing.extend_trace_label(trace_label, "zero_prob_utils"), + msg="all probabilities are zero", + trace_choosers=trace_choosers, + ) + + utils = pd.DataFrame(utils_arr, columns=utils.columns, index=utils.index) + + return utils + + def utils_to_probs( state: workflow.State, utils, @@ -274,6 +345,171 @@ def utils_to_probs( return probs, logsums return probs +FREEZE_RANDOM_NUMBERS_FOR_DENSE_ALTERNATIVE_SET = True + +@dataclass +class AltsContext: + """Representation of the alternatives without carrying around that full array.""" + min_alt_id: int + max_alt_id: int + + def __post_init__(self): + # e.g. for zero based zones max_alt_id = n_alts - 1 + # but for 1 based zones, we don't need to add extra padding + self.n_rands_to_sample = max(self.max_alt_id, self.n_alts_to_cover_max_id) + + @classmethod + def from_series(cls, ser:Union[pd.Series,pd.Index])->"AltsContext": + min_alt_id = ser.min() + max_alt_id = ser.max() + return cls(min_alt_id, max_alt_id) + + @classmethod + def from_num_alts(cls, num_alts:int, zero_based:bool=True)->"AltsContext": + if zero_based: + offset = -1 + else: + offset =0 + return cls(min_alt_id=1+offset, max_alt_id=num_alts+offset ) + + + @property + def n_alts_to_cover_max_id(self) -> int: + """If zones were non-consecutive, this could be a big over-estimate.""" + return self.max_alt_id+1 + + +# TODO-EET: add doc string, tracing +def add_ev1_random(state: workflow.State, df: pd.DataFrame, alt_info: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, ): + + nest_utils_for_choice = df.copy() + assert (alt_info is None) == ( + alt_nrs_df is None), "n_zones and alt_nrs_df must both be provided or omitted together" + + if alt_nrs_df is not None and FREEZE_RANDOM_NUMBERS_FOR_DENSE_ALTERNATIVE_SET: + assert alt_info is not None # narrowing for mypy + + idx_array = alt_nrs_df.values + mask = idx_array == -999 + safe_idx = np.where(mask, 1, idx_array) # replace -999 with a temp value inbounds + # generate random number for all alts - this is wasteful, but ensures that the same zone + # gets the same random number if the sampled choice set changes between base and project + # (alternatively, one could seed a channel for (persons x zones) and use the zone seed to ensure consistency. + # Trade off is needing to seed (persons x zones) rows and multiindex channels to + # avoid extra random numbers generated here. Quick benchmark suggests seeding per row is likely slower + rands_dense = state.get_rn_generator().gumbel_for_df(nest_utils_for_choice, n=alt_info.n_alts_to_cover_max_id) + # generate n=alt_info.max_alt_id+1 rather than n_alts so that indexing works + # (this is drawing a random number for a redundant zeroth zone in 1 based zoning systems) + # TODO deal with non 0->n-1 indexed land use more efficiently? ideally do where alt_nrs_df is constructed, + # not on the fly here. Potentially via state.get_injectable('network_los').get_skim_dict('taz').zone_ids + rands = np.take_along_axis(rands_dense, safe_idx, axis=1) + rands[mask] = 0 # zero out the masked zones so they don't have the util adjustment of alt 0 + else: + # old behaviour, to remove + rands = state.get_rn_generator().gumbel_for_df(nest_utils_for_choice, n=nest_utils_for_choice.shape[1]) + + nest_utils_for_choice += rands + return nest_utils_for_choice + + +def choose_from_tree( + nest_utils, all_alternatives, logit_nest_groups, nest_alternatives_by_name +): + for level, nest_names in logit_nest_groups.items(): + if level == 1: + next_level_alts = nest_alternatives_by_name[nest_names[0]] + continue + choice_this_level = nest_utils[nest_utils.index.isin(next_level_alts)].idxmax() + if choice_this_level in all_alternatives: + return choice_this_level + next_level_alts = nest_alternatives_by_name[choice_this_level] + raise ValueError("This should never happen - no alternative found") + + +# TODO-EET: add doc string, tracing +def make_choices_explicit_error_term_nl( + state, nested_utilities, alt_order_array, nest_spec, trace_label +): + """walk down the nesting tree and make choice at each level, which is the root of the next level choice.""" + nest_utils_for_choice = add_ev1_random(state, nested_utilities) + + all_alternatives = set(nest.name for nest in each_nest(nest_spec, type="leaf")) + logit_nest_groups = group_nest_names_by_level(nest_spec) + nest_alternatives_by_name = {n.name: n.alternatives for n in each_nest(nest_spec)} + + # Apply is slow. It could *maybe* be sped up by using the fact that the nesting structure is the same for all rows: + # Add ev1(0,1) to all entries (as is currently being done). Then, at each level, pick the maximum of the available + # composite alternatives and set the corresponding entry to 1 for each row, set all other alternatives at this level + # to zero. Once the tree is walked (all alternatives have been processed), take the product of the alternatives in + # each leaf's alternative list. Then pick the only alternative with entry 1, all others must be 0. + choices = nest_utils_for_choice.apply( + lambda x: choose_from_tree( + x, all_alternatives, logit_nest_groups, nest_alternatives_by_name + ), + axis=1, + ) + # TODO-EET: reporting like for zero probs + assert not choices.isnull().any(), f"No choice for {trace_label}" + choices = pd.Series(choices, index=nest_utils_for_choice.index) + + # In order for choice indexing to be consistent with MNL and cumsum MC choices, we need to index in the order + # alternatives were originally created before adding nest nodes that are not elemental alternatives + choices = choices.map({v: k for k, v in enumerate(alt_order_array)}) + + return choices + + +# TODO-EET: add doc string, tracing +def make_choices_explicit_error_term_mnl(state, utilities, trace_label, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, + ): + utilities_incl_unobs = add_ev1_random(state, utilities, alts_context, alt_nrs_df) + choices = np.argmax(utilities_incl_unobs.to_numpy(), axis=1) + # TODO-EET: reporting like for zero probs + assert not np.isnan(choices).any(), f"No choice for {trace_label}" + choices = pd.Series(choices, index=utilities_incl_unobs.index) + return choices + + +def make_choices_explicit_error_term( + state, utilities, alt_order_array, nest_spec=None, trace_label=None, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +): + trace_label = tracing.extend_trace_label(trace_label, "make_choices_eet") + if nest_spec is None: + choices = make_choices_explicit_error_term_mnl(state, utilities, trace_label, alts_context, alt_nrs_df) + else: + choices = make_choices_explicit_error_term_nl( + state, utilities, alt_order_array, nest_spec, trace_label + ) + return choices + + +def make_choices_utility_based( + state: workflow.State, + utilities: pd.DataFrame, + name_mapping=None, + nest_spec=None, + trace_label: str | None = None, + trace_choosers=None, + allow_bad_probs=False, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +) -> tuple[pd.Series, pd.Series]: + trace_label = tracing.extend_trace_label(trace_label, "make_choices_utility_based") + + # TODO-EET: index of choices for nested utilities is different than unnested - this needs to be consistent for + # turning indexes into alternative names to keep code changes to minimum for now + choices = make_choices_explicit_error_term( + state, utilities, name_mapping, nest_spec, trace_label, alts_context, alt_nrs_df + ) + # TODO-EET: rands - log all zeros for now + rands = pd.Series(np.zeros_like(utilities.index.values), index=utilities.index) + return choices, rands + def make_choices( state: workflow.State, @@ -284,28 +520,23 @@ def make_choices( ) -> tuple[pd.Series, pd.Series]: """ Make choices for each chooser from among a set of alternatives. - Parameters ---------- probs : pandas.DataFrame Rows for choosers and columns for the alternatives from which they are choosing. Values are expected to be valid probabilities across each row, e.g. they should sum to 1. - trace_choosers : pandas.dataframe the choosers df (for interaction_simulate) to facilitate the reporting of hh_id by report_bad_choices because it can't deduce hh_id from the interaction_dataset which is indexed on index values from alternatives df - Returns ------- choices : pandas.Series Maps chooser IDs (from `probs` index) to a choice, where the choice is an index into the columns of `probs`. - rands : pandas.Series The random numbers used to make the choices (for debugging, tracing) - """ trace_label = tracing.extend_trace_label(trace_label, "make_choices") @@ -546,6 +777,7 @@ def _each_nest(spec: LogitNestSpec, parent_nest, post_order): nest.level = parent_nest.level + 1 nest.product_of_coefficients = parent_nest.product_of_coefficients nest.ancestors = parent_nest.ancestors + [name] + nest.coefficient = parent_nest.coefficient yield spec, nest @@ -602,3 +834,12 @@ def count_each_nest(spec, count): return 1 return count_each_nest(nest_spec, 0) if nest_spec is not None else 0 + + +def group_nest_names_by_level(nest_spec): + # group nests by level, returns {level: [nest.name at that level]} + depth = np.max([x.level for x in each_nest(nest_spec)]) + nest_levels = {x: [] for x in range(1, depth + 1)} + for n in each_nest(nest_spec): + nest_levels[n.level].append(n.name) + return nest_levels diff --git a/activitysim/core/pathbuilder.py b/activitysim/core/pathbuilder.py index 31393ceeaf..da241addcd 100644 --- a/activitysim/core/pathbuilder.py +++ b/activitysim/core/pathbuilder.py @@ -991,16 +991,16 @@ def build_virtual_path( utilities_df.index = orig.index with memo("#TVPB build_virtual_path make_choices"): - probs = logit.utils_to_probs( - self.network_los.state, - utilities_df, - allow_zero_probs=True, - trace_label=trace_label, - overflow_protection=False, - ) - chunk_sizer.log_df(trace_label, "probs", probs) - if trace: + probs = logit.utils_to_probs( + self.network_los.state, + utilities_df, + allow_zero_probs=True, + trace_label=trace_label, + overflow_protection=False, + ) + chunk_sizer.log_df(trace_label, "probs", probs) + choices = override_choices utilities_df["choices"] = choices @@ -1008,21 +1008,44 @@ def build_virtual_path( probs["choices"] = choices self.trace_df(probs, trace_label, "probs") + del probs + chunk_sizer.log_df(trace_label, "probs", None) else: - choices, rands = logit.make_choices( - self.network_los.state, - probs, - allow_bad_probs=True, - trace_label=trace_label, - ) + if self.network_los.state.settings.use_explicit_error_terms: + utilities_df = logit.validate_utils( + self.network_los.state, + utilities_df, + allow_zero_probs=True, + trace_label=trace_label, + ) + choices, rands = logit.make_choices_utility_based( + self.network_los.state, + utilities_df, + trace_label=trace_label, + ) + else: + probs = logit.utils_to_probs( + self.network_los.state, + utilities_df, + allow_zero_probs=True, + trace_label=trace_label, + overflow_protection=False, + ) + chunk_sizer.log_df(trace_label, "probs", probs) + + choices, rands = logit.make_choices( + self.network_los.state, + probs, + allow_bad_probs=True, + trace_label=trace_label, + ) + del probs + chunk_sizer.log_df(trace_label, "probs", None) chunk_sizer.log_df(trace_label, "rands", rands) del rands chunk_sizer.log_df(trace_label, "rands", None) - del probs - chunk_sizer.log_df(trace_label, "probs", None) - # we need to get path_set, btap, atap from path_df row with same seq and path_num # drop seq join column, but keep path_num of choice to override_choices when tracing columns_to_cache = ["btap", "atap", "path_set", "path_num"] diff --git a/activitysim/core/random.py b/activitysim/core/random.py index 37b1976403..79e6f7e7a4 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -9,8 +9,8 @@ import numpy as np import pandas as pd -from activitysim.core.util import reindex from activitysim.core.exceptions import DuplicateLoadableObjectError, TableIndexError +from activitysim.core.util import reindex from .tracing import print_elapsed_time @@ -194,6 +194,8 @@ def _generators_for_df(self, df): df_row_states = self.row_states.loc[df.index] + # https://numpy.org/doc/stable/reference/random/generator.html + # np.random.default_rng() prng = np.random.RandomState() for row in df_row_states.itertuples(): prng.seed(row.row_seed) @@ -245,6 +247,50 @@ def random_for_df(self, df, step_name, n=1): self.row_states.loc[df.index, "offset"] += n return rands + def gumbel_for_df(self, df, step_name, n=1): + """ + Return n floating point gumbel-distributed numbers for each row in df + using the appropriate random channel for each row. + + Subsequent calls (in the same step) will return the next rand for each df row + + The resulting array will be the same length (and order) as df + This method is designed to support alternative selection from a probability array + + The columns in df are ignored; the index name and values are used to determine + which random number sequence to to use. + + If "true pseudo random" behavior is desired (i.e. NOT repeatable) the set_base_seed + method (q.v.) may be used to globally reseed all random streams. + + Parameters + ---------- + df : pandas.DataFrame + df with index name and values corresponding to a registered channel + + n : int + number of rands desired per df row + + Returns + ------- + rands : 2-D ndarray + array the same length as df, with n floats in range [0, 1) for each df row + """ + + assert self.step_name + assert self.step_name == step_name + + # - reminder: prng must be called when yielded as generated sequence, not serialized + generators = self._generators_for_df(df) + + # rands = np.asanyarray([prng.gumbel(size=n) for prng in generators]) + # this is about 20% faster for large arrays, like for destination choice + rands = np.asanyarray([-np.log(-np.log(prng.rand(n))) for prng in generators]) + + # update offset for rows we handled + self.row_states.loc[df.index, "offset"] += n + return rands + def normal_for_df(self, df, step_name, mu, sigma, lognormal=False, size=None): """ Return a floating point random number in normal (or lognormal) distribution @@ -617,6 +663,42 @@ def random_for_df(self, df, n=1): rands = channel.random_for_df(df, self.step_name, n) return rands + def gumbel_for_df(self, df, n=1): + """ + Return a single floating point gumbel for each row in df + using the appropriate random channel for each row. + + Subsequent calls (in the same step) will return the next rand for each df row + + The resulting array will be the same length (and order) as df + This method is designed to support alternative selection from a probability array + + The columns in df are ignored; the index name and values are used to determine + which random number sequence to to use. + + We assume that we can identify the channel to used based on the name of df.index + This channel should have already been registered by a call to add_channel (q.v.) + + If "true pseudo random" behavior is desired (i.e. NOT repeatable) the set_base_seed + method (q.v.) may be used to globally reseed all random streams. + + Parameters + ---------- + df : pandas.DataFrame + df with index name and values corresponding to a registered channel + + n : int + number of rands desired (default 1) + + Returns + ------- + choices : 1-D ndarray the same length as df + a single float in range [0, 1) for each row in df + """ + channel = self.get_channel_for_df(df) + rands = channel.gumbel_for_df(df, self.step_name, n) + return rands + def normal_for_df(self, df, mu=0, sigma=1, broadcast=False, size=None): """ Return a single floating point normal random number in range (-inf, inf) for each row in df diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index c38e16a850..bf9c23f5d7 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -4,6 +4,7 @@ import logging import time +import typing import warnings from collections import OrderedDict from collections.abc import Callable @@ -32,7 +33,8 @@ LogitNestSpec, TemplatedLogitComponentSettings, ) -from activitysim.core.estimation import Estimator + +from activitysim.core.exceptions import ModelConfigurationError from activitysim.core.fast_eval import fast_eval from activitysim.core.simulate_consts import ( ALT_LOSER_UTIL, @@ -40,7 +42,9 @@ SPEC_EXPRESSION_NAME, SPEC_LABEL_NAME, ) -from activitysim.core.exceptions import ModelConfigurationError +if typing.TYPE_CHECKING: + from activitysim.core.estimation import Estimator + logger = logging.getLogger(__name__) @@ -1093,6 +1097,46 @@ def set_skim_wrapper_targets(df, skims, allow_partial_success: bool = True): # ) +def compute_nested_utilities(raw_utilities, nest_spec): + """ + compute nest utilities based on nesting coefficients + + For nest nodes this is the logsum of alternatives adjusted by nesting coefficient + + leaf <- raw_utility / nest_coefficient + nest <- ln(sum of exponentiated raw_utility of leaves) * nest_coefficient) + + Parameters + ---------- + raw_utilities : pandas.DataFrame + dataframe with the raw alternative utilities of all leaves + (what in non-nested logit would be the utilities of all the alternatives) + nest_spec : dict + Nest tree dict from the model spec yaml file + + Returns + ------- + nested_utilities : pandas.DataFrame + Will have the index of `raw_utilities` and columns for leaf and node utilities + """ + nested_utilities = pd.DataFrame(index=raw_utilities.index) + + for nest in logit.each_nest(nest_spec, post_order=True): + name = nest.name + if nest.is_leaf: + nested_utilities[name] = ( + raw_utilities[name].astype(float) / nest.product_of_coefficients + ) + else: + # the alternative nested_utilities will already have been computed due to post_order + with np.errstate(divide="ignore"): + nested_utilities[name] = nest.coefficient * np.log( + np.exp(nested_utilities[nest.alternatives]).sum(axis=1) + ) + + return nested_utilities + + def compute_nested_exp_utilities(raw_utilities, nest_spec): """ compute exponentiated nest utilities based on nesting coefficients @@ -1224,7 +1268,7 @@ def eval_mnl( choosers, spec, locals_d, - custom_chooser: CustomChooser_T, + custom_chooser: CustomChooser_T | None, estimator, log_alt_losers=False, want_logsums=False, @@ -1308,29 +1352,47 @@ def eval_mnl( column_labels=["alternative", "utility"], ) - probs = logit.utils_to_probs( - state, utilities, trace_label=trace_label, trace_choosers=choosers - ) - chunk_sizer.log_df(trace_label, "probs", probs) + if state.settings.use_explicit_error_terms: + utilities = logit.validate_utils( + state, utilities, trace_label=trace_label, trace_choosers=choosers + ) - del utilities - chunk_sizer.log_df(trace_label, "utilities", None) + if custom_chooser: + choices, rands = custom_chooser( + state, utilities, choosers, spec, trace_label + ) + else: + choices, rands = logit.make_choices_utility_based( + state, utilities, trace_label=trace_label + ) - if have_trace_targets: - # report these now in case make_choices throws error on bad_choices - state.tracing.trace_df( - probs, - "%s.probs" % trace_label, - column_labels=["alternative", "probability"], - ) + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) - if custom_chooser: - choices, rands = custom_chooser(state, probs, choosers, spec, trace_label) else: - choices, rands = logit.make_choices(state, probs, trace_label=trace_label) + probs = logit.utils_to_probs( + state, utilities, trace_label=trace_label, trace_choosers=choosers + ) + chunk_sizer.log_df(trace_label, "probs", probs) - del probs - chunk_sizer.log_df(trace_label, "probs", None) + del utilities + chunk_sizer.log_df(trace_label, "utilities", None) + + if have_trace_targets: + # report these now in case make_choices throws error on bad_choices + state.tracing.trace_df( + probs, + "%s.probs" % trace_label, + column_labels=["alternative", "probability"], + ) + + if custom_chooser: + choices, rands = custom_chooser(state, probs, choosers, spec, trace_label) + else: + choices, rands = logit.make_choices(state, probs, trace_label=trace_label) + + del probs + chunk_sizer.log_df(trace_label, "probs", None) if have_trace_targets: state.tracing.trace_df( @@ -1431,87 +1493,140 @@ def eval_nl( column_labels=["alternative", "utility"], ) - # exponentiated utilities of leaves and nests - nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) - chunk_sizer.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) + if state.settings.use_explicit_error_terms: + # TODO-EET: Nested utility zero choice probability + raw_utilities = logit.validate_utils( + state, raw_utilities, allow_zero_probs=True, trace_label=trace_label + ) - del raw_utilities - chunk_sizer.log_df(trace_label, "raw_utilities", None) + # utilities of leaves and nests + nested_utilities = compute_nested_utilities(raw_utilities, nest_spec) + chunk_sizer.log_df(trace_label, "nested_utilities", nested_utilities) - if have_trace_targets: - state.tracing.trace_df( - nested_exp_utilities, - "%s.nested_exp_utilities" % trace_label, - column_labels=["alternative", "utility"], - ) + # TODO-EET: use nested_utiltites directly to compute logsums? + if want_logsums: + # logsum of nest root + # exponentiated utilities of leaves and nests + nested_exp_utilities = compute_nested_exp_utilities( + raw_utilities, nest_spec + ) + chunk_sizer.log_df( + trace_label, "nested_exp_utilities", nested_exp_utilities + ) + logsums = pd.Series(np.log(nested_exp_utilities.root), index=choosers.index) + chunk_sizer.log_df(trace_label, "logsums", logsums) + + # TODO-EET: index of choices for nested utilities is different than unnested - this needs to be consistent for + # turning indexes into alternative names to keep code changes to minimum for now + name_mapping = raw_utilities.columns.values + + del raw_utilities + chunk_sizer.log_df(trace_label, "raw_utilities", None) + + if custom_chooser: + choices, rands = custom_chooser( + state, + utilities=nested_utilities, + name_mapping=name_mapping, + choosers=choosers, + spec=spec, + nest_spec=nest_spec, + trace_label=trace_label, + ) + else: + choices, rands = logit.make_choices_utility_based( + state, + nested_utilities, + name_mapping=name_mapping, + nest_spec=nest_spec, + trace_label=trace_label, + ) - # probabilities of alternatives relative to siblings sharing the same nest - nested_probabilities = compute_nested_probabilities( - state, nested_exp_utilities, nest_spec, trace_label=trace_label - ) - chunk_sizer.log_df(trace_label, "nested_probabilities", nested_probabilities) + del nested_utilities + chunk_sizer.log_df(trace_label, "nested_utilities", None) - if want_logsums: - # logsum of nest root - logsums = pd.Series(np.log(nested_exp_utilities.root), index=choosers.index) - chunk_sizer.log_df(trace_label, "logsums", logsums) + else: + # exponentiated utilities of leaves and nests + nested_exp_utilities = compute_nested_exp_utilities(raw_utilities, nest_spec) + chunk_sizer.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) - del nested_exp_utilities - chunk_sizer.log_df(trace_label, "nested_exp_utilities", None) + del raw_utilities + chunk_sizer.log_df(trace_label, "raw_utilities", None) - if have_trace_targets: - state.tracing.trace_df( - nested_probabilities, - "%s.nested_probabilities" % trace_label, - column_labels=["alternative", "probability"], + if have_trace_targets: + state.tracing.trace_df( + nested_exp_utilities, + "%s.nested_exp_utilities" % trace_label, + column_labels=["alternative", "utility"], + ) + + # probabilities of alternatives relative to siblings sharing the same nest + nested_probabilities = compute_nested_probabilities( + state, nested_exp_utilities, nest_spec, trace_label=trace_label ) + chunk_sizer.log_df(trace_label, "nested_probabilities", nested_probabilities) - # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) - base_probabilities = compute_base_probabilities( - nested_probabilities, nest_spec, spec - ) - chunk_sizer.log_df(trace_label, "base_probabilities", base_probabilities) + if want_logsums: + # logsum of nest root + logsums = pd.Series(np.log(nested_exp_utilities.root), index=choosers.index) + chunk_sizer.log_df(trace_label, "logsums", logsums) - del nested_probabilities - chunk_sizer.log_df(trace_label, "nested_probabilities", None) + del nested_exp_utilities + chunk_sizer.log_df(trace_label, "nested_exp_utilities", None) - if have_trace_targets: - state.tracing.trace_df( - base_probabilities, - "%s.base_probabilities" % trace_label, - column_labels=["alternative", "probability"], + if have_trace_targets: + state.tracing.trace_df( + nested_probabilities, + "%s.nested_probabilities" % trace_label, + column_labels=["alternative", "probability"], + ) + + # global (flattened) leaf probabilities based on relative nest coefficients (in spec order) + base_probabilities = compute_base_probabilities( + nested_probabilities, nest_spec, spec ) + chunk_sizer.log_df(trace_label, "base_probabilities", base_probabilities) - # note base_probabilities could all be zero since we allowed all probs for nests to be zero - # check here to print a clear message but make_choices will raise error if probs don't sum to 1 - BAD_PROB_THRESHOLD = 0.001 - no_choices = (base_probabilities.sum(axis=1) - 1).abs() > BAD_PROB_THRESHOLD + del nested_probabilities + chunk_sizer.log_df(trace_label, "nested_probabilities", None) - if no_choices.any(): - logit.report_bad_choices( - state, - no_choices, - base_probabilities, - trace_label=tracing.extend_trace_label(trace_label, "bad_probs"), - trace_choosers=choosers, - msg="base_probabilities do not sum to one", - ) + if have_trace_targets: + state.tracing.trace_df( + base_probabilities, + "%s.base_probabilities" % trace_label, + column_labels=["alternative", "probability"], + ) - if custom_chooser: - choices, rands = custom_chooser( - state, - base_probabilities, - choosers, - spec, - trace_label, - ) - else: - choices, rands = logit.make_choices( - state, base_probabilities, trace_label=trace_label - ) + # note base_probabilities could all be zero since we allowed all probs for nests to be zero + # check here to print a clear message but make_choices will raise error if probs don't sum to 1 + BAD_PROB_THRESHOLD = 0.001 + no_choices = (base_probabilities.sum(axis=1) - 1).abs() > BAD_PROB_THRESHOLD + + if no_choices.any(): + logit.report_bad_choices( + state, + no_choices, + base_probabilities, + trace_label=tracing.extend_trace_label(trace_label, "bad_probs"), + trace_choosers=choosers, + msg="base_probabilities do not sum to one", + ) + + if custom_chooser: + choices, rands = custom_chooser( + state, + base_probabilities, + choosers, + spec, + trace_label, + ) + else: + choices, rands = logit.make_choices( + state, base_probabilities, trace_label=trace_label + ) - del base_probabilities - chunk_sizer.log_df(trace_label, "base_probabilities", None) + del base_probabilities + chunk_sizer.log_df(trace_label, "base_probabilities", None) if have_trace_targets: state.tracing.trace_df( diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index e249475de2..30bae8a3b9 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -3,13 +3,16 @@ from __future__ import annotations import os.path +import re import numpy as np import pandas as pd import pandas.testing as pdt import pytest -from activitysim.core import logit, workflow + +from activitysim.core import logit, workflow, random +from activitysim.core.logit import add_ev1_random, AltsContext from activitysim.core.simulate import eval_variables @@ -70,6 +73,9 @@ def utilities(choosers, spec, test_data): ) +# TODO-EET: Add tests here! + + def test_utils_to_probs(utilities, test_data): state = workflow.State().default_settings() probs = logit.utils_to_probs(state, utilities, trace_label=None) @@ -127,6 +133,65 @@ def test_make_choices_only_one(): choices, pd.Series([0, 1], index=["x", "y"]), check_dtype=False ) +def reset_step(state, name='test_step'): + state.get_rn_generator().end_step(name) + state.get_rn_generator().begin_step(name) + +def test_make_choices_utility_based_sampled_alts(): + """Test the situation of making choices from a sampled choice set""" + # TODO should these tests go in test_random? + state = workflow.State().default_settings() + # Make explicit that there's two indexing schemes - the raw alts, and the 0 based internals + utils_project_raw = pd.DataFrame({"a":10.582999, "b":10.680792, "c":10.710443}, index=pd.Index([0], name='person_id')) + # zero based indexes + utils_project = utils_project_raw.rename(columns={"a":0, "b":1, "c":2}) + utils_base = utils_project_raw[["a", "c"]].rename(columns={"a":0, "c":1}) + + assert utils_project.index.name == "person_id" + state.get_rn_generator().add_channel("persons", utils_project) + state.get_rn_generator().begin_step("test_step") + # mock base case, where alt 1 is omitted (it was improved in the project) + # this situation is quite common with poisson sampling with a variable choice set size, + # but it can also happen in with-replacement EET sampling e.g. if alt 2 had a pick_count of 2 in the base case. + # In principle, it can also be problematic for non-sampled choices where there is a base project difference in the + # availability of alternatives .e.g a new mode was introduced in the project case + + utils_project_with_rands = add_ev1_random(state, utils_project) + rands_project = utils_project_with_rands - utils_project + reset_step(state) + utils_base_with_rands = add_ev1_random(state, utils_base) + rands_base = utils_base_with_rands - utils_base + rands_base_labeled = rands_base.rename(columns={0:"a", 1:"c"}) + rands_project_labeled = rands_project.rename(columns={0:"a", 1:"b", 2:"c"}) + with pytest.raises(AssertionError, match=re.escape('(column name="c") are different')): + # TODO this should pass + pdt.assert_frame_equal(rands_base_labeled, rands_project_labeled.loc[:, rands_base_labeled.columns]) + # document incorrect invariant - first two columns have the same random numbers: + pdt.assert_frame_equal(rands_base, rands_project.iloc[:, :2]) + + # revised approach + reset_step(state) + alt_nrs_df = pd.DataFrame({0:0, 1:1, 2:2}, index=utils_project_raw.index) + alt_info = AltsContext.from_num_alts(3, zero_based=True) + utils_project_with_rands = add_ev1_random(state, utils_project, alt_info=alt_info, alt_nrs_df=alt_nrs_df) + rands_project = utils_project_with_rands - utils_project + reset_step(state) + + # alt "b" is missing from the sampled choice set, alt_nrs_df is set to reflect that + alt_nrs_df = pd.DataFrame({0: 0, 1: 2}, index=utils_project_raw.index) + utils_base_with_rands = add_ev1_random(state, utils_base, alt_info=alt_info, alt_nrs_df=alt_nrs_df) + rands_base = utils_base_with_rands - utils_base + rands_base_labeled = rands_base.rename(columns={0: "a", 1: "c"}) + rands_project_labeled = rands_project.rename(columns={0: "a", 1: "b", 2: "c"}) + + # Corrected invariant holds true + pdt.assert_frame_equal(rands_base_labeled, rands_project_labeled.loc[:, rands_base_labeled.columns]) + + + + + + def test_make_choices_real_probs(utilities): state = workflow.State().default_settings() From 6782b17cd86ebbceb7266e5033d52121c32153e1 Mon Sep 17 00:00:00 2001 From: Matt Richards Date: Sat, 9 May 2026 21:57:15 +1000 Subject: [PATCH 2/2] Squashed commit of the following: (Squash of https://github.com/ActivitySim/activitysim/pull/1064 as of fcef6b3) Co-authored-by: Jan Co-authored-by: Tom Stephen Co-authored-by: Tyler Pearn commit fcef6b3b305a55ecd984f7ffa69705ba83979a4f Merge: 08b2b033f 10db12396 Author: Jan Date: Sat Apr 11 19:34:40 2026 +1000 Merge pull request #7 from outerl/deterministic-alt-sample-randoms Deterministic alt sample randoms commit 10db12396c768bb07596b87ae0b4c7c1493b024e Author: Jan Zill Date: Sat Apr 11 17:26:45 2026 +1000 doco update commit e9fb130f21e240e4ebd9539c645c02e5e241aaa7 Author: Jan Zill Date: Sat Apr 11 16:31:27 2026 +1000 disable arc sharrow test until regress trips are updated commit d7c13456b20b2d64c4dac303c5b5f356c7dfb6b3 Author: Jan Zill Date: Sat Apr 11 14:16:20 2026 +1000 trip scheduling comment for EET commit f2d7bfad6c0bfa05312fa611622495da8a301d87 Author: Jan Zill Date: Sat Apr 11 13:44:23 2026 +1000 removes outdated comment commit 8d7ae417629d43fdcbc8192055c7b59261c594a6 Author: Jan Zill Date: Sat Apr 11 13:25:01 2026 +1000 updates arc regress files commit 3bd32515232b802d85cfa0672107aada023a29ac Author: Jan Zill Date: Sat Apr 11 13:09:51 2026 +1000 no alts_context for trip_scheduling commit 12a3d8acc74816c2abcb6a3fd75ca91440e768da Author: Jan Zill Date: Sat Apr 11 13:05:01 2026 +1000 Revert "Deterministic randoms when adding/removing tours for trip scheduling choice" This reverts commit 2d181b2a9ada7d22d7ed8e8f7765360e9150d2ba. commit 6d29f18ecc43afb3638dd678f86c93076bc2f499 Author: Jan Zill Date: Sat Apr 11 13:01:59 2026 +1000 re-instate arc tests commit 834fc907f2266c04f63cf545e1b684727422dda5 Author: Jan Zill Date: Sat Apr 11 08:56:08 2026 +1000 re-add arc test, remove parking choice column from regress trips commit 19750a0921e4d61d55f5ed475d3bd01ca717542d Author: Jan Zill Date: Sat Apr 11 07:15:25 2026 +1000 updates outdated comment commit 531b56238fe717b92678188710fd793101415774 Author: Tom Stephen Date: Fri Apr 10 16:44:15 2026 +1000 linting commit bc56a8771071702d1df3ead232fa161be6ab865e Author: Tom Stephen Date: Fri Apr 10 16:32:14 2026 +1000 PR noise, make sure alts_context built on un-modified alternatives in trip_dest commit 7a22612e0c3b7c0bbd77b099cd4ddc84cbbb46df Author: Tom Stephen Date: Fri Apr 10 15:00:25 2026 +1000 Roll back final_trips.csv for production_semcog commit cfe616657b6ea4bd3319c628fee865969397e4cc Author: Tom Stephen Date: Fri Apr 10 11:47:52 2026 +1000 PR noise commit 2d181b2a9ada7d22d7ed8e8f7765360e9150d2ba Author: Tom Stephen Date: Fri Apr 10 11:30:08 2026 +1000 Deterministic randoms when adding/removing tours for trip scheduling choice commit 47ed6000ae4e6828845fb375eda0e7810ee6b672 Author: Tom Stephen Date: Fri Apr 10 09:46:03 2026 +1000 Commenting/fix alts context constructions, update semcog test commit 89dbe5c1d83dc51e316f9974695d495b737be0bc Author: Tom Stephen Date: Thu Apr 9 17:47:30 2026 +1000 Tidy, complete implementation of alts_context, add tests, rebuild regress for multi zone tests commit 0d2356e3cc43e38b2116fb97f4eacd11c75d916f Author: Tom Stephen Date: Thu Apr 9 13:46:47 2026 +1000 Copy changes from m-richards:matt/poisson_eet_2026_04 commit 08b2b033f6b4cce6a5ff7abf78ee5756026d38d2 Merge: d5a8fd46f c7869193a Author: Jan Date: Thu Apr 9 09:28:15 2026 +1000 Merge pull request #6 from outerl/explicit_error_terms-multiple_zone_test Adds 2 zone tests commit c7869193a59657142c33f56842db7246dd33ad3c Author: Jan Zill Date: Tue Apr 7 22:17:33 2026 +1000 progressive 2 zone test commit 7c96cbc69c8c273cc2f6ef48fc1836c7acdad124 Author: Jan Zill Date: Tue Apr 7 20:08:58 2026 +1000 base seed same as non-eet, update regress eet data commit 9661d01c9c7c5802e42fbbedaa9668b6e7fbda8f Author: Jan Zill Date: Tue Apr 7 19:17:37 2026 +1000 re-add 3-zone tests commit 5b24d44ec4d3ed50d902b244995f0f4eda6e2843 Author: Jan Zill Date: Tue Apr 7 19:11:51 2026 +1000 add back accidentally removed import commit f5b63014859a43c390f37bf6c1e54508f07efbfc Author: Tyler Pearn Date: Tue Apr 7 12:47:25 2026 +1000 Change formatting from using ruff to black commit 033f08a997607178c32898056b96579b7464885b Author: Tyler Pearn Date: Tue Apr 7 12:42:10 2026 +1000 Add eet multiple zone test for zone=2 commit d5a8fd46fe046f69f508eafe37f1edc0498f345e Author: Jan Zill Date: Fri Apr 3 20:47:48 2026 -0300 no need to restrict to specific choosers for reset, each process has its own state commit d50ad3c1a7d66ee515b00ba35ab89b258be5cfc4 Author: Jan Zill Date: Fri Apr 3 16:41:33 2026 +1000 semcog test with different seed for eet mp to work around non-determinism in trip_scheduling (probabilistic, no EET branch) commit 220c5cfe50c0cb871017d1662d9a1e9b15e3f4c8 Author: Jan Zill Date: Fri Apr 3 10:41:15 2026 +1000 mp rng reset, disabling one new test for no commit 22a4f6277d046920eb22efcd040ed74d7889552c Author: Jan Zill Date: Fri Apr 3 08:59:54 2026 +1000 disable test unitl further investigation commit 720998556bdc8e80908b51b3adf0d4d6d2bd6150 Author: Jan Zill Date: Fri Apr 3 08:41:52 2026 +1000 lint commit be3a270ce819f5de98d861aee3ef457a7174cd4d Author: Jan Zill Date: Fri Apr 3 08:35:58 2026 +1000 testing for semcog model, update to shadow pricing reset commit c5bd23f4e91f21c53a3a801d37480c506e42394f Merge: 5604b3e6c a7e8529d6 Author: Jan Date: Fri Apr 3 04:28:44 2026 +1000 Merge pull request #3 from outerl/jzill/runtime_improvement Testing, documentation, runtime improvement commit a7e8529d6be103ced9719e1cd0e0ac5ea0597592 Author: Jan Zill Date: Thu Apr 2 21:52:41 2026 +1000 undo changes in unrelated code to reduce noise commit 7959c9411c7fbaab6c969fe3b42b78b1208ff959 Author: Jan Zill Date: Thu Apr 2 21:38:03 2026 +1000 clean up doco commit d674268da43b93148e4d93664e2c7b6c8fae5483 Author: Jan Zill Date: Thu Apr 2 21:14:39 2026 +1000 memory usage doco commit 837ae417fb65a16c2da37fea4ea5d9f4242130bb Author: Jan Zill Date: Thu Apr 2 21:03:06 2026 +1000 doco clean up commit 2d1dbcb89536eb07595abc5d0166b050d83015a2 Author: Jan Zill Date: Thu Apr 2 20:59:29 2026 +1000 doco re memory commit f542e58f206dbb62ec10c116c8779ec711868dca Author: Jan Zill Date: Thu Apr 2 20:53:58 2026 +1000 doco clean up commit 8c74d2a5be97017a1f29f5595bf7fb4383ca3db1 Author: Jan Zill Date: Thu Apr 2 16:55:08 2026 +1000 docs commit 605366fc80931885a051e27bc9c6481f99b415f1 Author: Jan Zill Date: Thu Apr 2 16:38:41 2026 +1000 doc fix commit 1d139e08cfbd759a3d6bc2face83051e01cf1093 Author: Tom Stephen Date: Thu Apr 2 10:10:30 2026 +1000 Fix failing tests commit 0da80bf860294c81c80a32e24abd132c031b853f Author: Tom Stephen Date: Thu Apr 2 09:47:52 2026 +1000 . commit 21daa5c0b50c2aefb44de2957ad89cfa30704e8c Author: Tom Stephen Date: Thu Apr 2 09:44:16 2026 +1000 Revert linting changes commit 40b0b0611019ce63dc13551955092e56593d229d Author: Jan Zill Date: Tue Mar 31 21:58:15 2026 +1000 add scale comment commit a0bc5ba0ac51b24dc07f0674e83ba98082f4b374 Merge: 661c6b386 b455e2afe Author: Jan Zill Date: Tue Mar 31 21:54:33 2026 +1000 Merge branch 'jzill/runtime_improvement' of github.com:outerl/activitysim into jzill/runtime_improvement commit 661c6b386699ebf0aa619346b844da1c15662b06 Author: Jan Zill Date: Tue Mar 31 21:53:39 2026 +1000 doco part two commit a490999591cc2ba94e5c251592745b62d39624c8 Author: Jan Zill Date: Tue Mar 31 20:34:51 2026 +1000 eet doco start commit b455e2afe44c6607a078b937fd65c4771d24e973 Author: Tom Stephen Date: Tue Mar 31 17:02:02 2026 +1000 Roll back validate_utils commit a73f6ea11f94085d7ef275d21f6a775bec7d526f Author: Tom Stephen Date: Tue Mar 31 16:28:31 2026 +1000 Fix test commit dd547115b12d6520c4785fa7b10adc02940bbc65 Author: Tom Stephen Date: Tue Mar 31 16:06:18 2026 +1000 Linting commit 327047443204db971018df7b7d73176477d2a1fb Author: Tom Stephen Date: Tue Mar 31 16:01:53 2026 +1000 Shuffle check to logit.py commit c1806fc619b981f70a217fec2a5339ed0b1a6ffb Author: Tom Stephen Date: Tue Mar 31 15:56:31 2026 +1000 Close out remaining TODO-EETs commit cc8fda0f190921f662d7536f28cbdb7d92a6dbc0 Author: Jan Zill Date: Tue Mar 31 15:19:41 2026 +1000 reset rng only for eet for now. discuss in engineering meeting commit d491e45d99163f4966001b5534adcf7bfd1cea09 Merge: 1aa85ba4b 6ef25e12d Author: Jan Zill Date: Tue Mar 31 15:10:05 2026 +1000 Merge branch 'jzill/runtime_improvement' into jzill/reset_rng_on_shadow_pricing commit 6ef25e12d6c674aa2143c99e7c0dfa3e6a7fdd4d Author: Jan Zill Date: Tue Mar 31 13:40:26 2026 +1000 clean up commit ae714330244aa529eb49c0c3d5275beb9dd9ad27 Author: Jan Zill Date: Tue Mar 31 13:14:09 2026 +1000 lint commit 52f75fa99cb5a9dc1b9a49fc7c0fba22b40f9b20 Merge: 4e0c7e8db b021cee2b Author: Jan Date: Tue Mar 31 13:10:19 2026 +1000 Merge pull request #5 from outerl/tom/nested-logit-tests-and-docs Extend logit tests, add model tests, add simulate tests, draft docs commit b021cee2bbd214c655952034f37c3c4249e1ebaa Author: Tyler Pearn Date: Tue Mar 31 11:29:12 2026 +1000 Finish removing nest_spec definition in simulate.py tests commit b62001e70104e226965d76cf8761b105b772577a Merge: 743c56fd9 4debaadb9 Author: Tyler Pearn Date: Tue Mar 31 11:28:20 2026 +1000 Merge with Tom's linting changes commit 743c56fd93fd4ec4a38d7df0633df35bdece1fcb Author: Tyler Pearn Date: Tue Mar 31 11:20:25 2026 +1000 Move nest_spec to a fixture in simulate.py tests commit 4debaadb9c8095ac17d64e52ad1b6216b242f239 Author: Tom Stephen Date: Tue Mar 31 11:17:03 2026 +1000 Linting commit f55b2cdce728aa429b6b15b69595b72b02e89a42 Author: Tyler Pearn Date: Tue Mar 31 10:59:30 2026 +1000 Add test for compute_nested_utilities. Computes nested utilities on a toy example and compares the result commit 65fd171ca9acc3b11fa46faf60cf284dbf09c7bf Author: Tyler Pearn Date: Tue Mar 31 08:50:37 2026 +1000 Implement Jan's suggestion of how to calculate household ids for testing cdap eet parity commit d0a64296fc5fe42221c337d371395a47c879d08d Author: Jan Zill Date: Thu Mar 26 20:09:33 2026 +1000 roll back changes to core tests to minimize noise commit 362eef0884c266aa7e09c6ac4c6c4200ad4c8ba6 Author: Tom Stephen Date: Tue Mar 24 09:37:18 2026 +1000 Linting, minor changes to test_simulate.py commit f628505215353849351534f9f1c77fb9cac4bbb2 Author: Tyler Pearn Date: Mon Mar 23 17:02:46 2026 +1000 Add tests checking that choices made using eet and from probabilities are the same for eval_mnl and eval_nl commit 76cacb2242e7d18fb4b8b6d720919e21aab139f2 Author: Tom Stephen Date: Mon Mar 23 17:19:37 2026 +1000 Add basic docs for EET commit bd4211f6e638a364b5ae44c56374e6f1af7747b4 Author: Tom Stephen Date: Mon Mar 23 16:38:56 2026 +1000 Add tests for logit NL, ordering, and all models using EET commit 1aa85ba4b8e3a0f06d34bf705c6f89b34a746deb Author: Jan Zill Date: Sun Mar 22 16:36:23 2026 +1000 reset rng offset on iterate_location_choice (shadow pricing) commit 4e0c7e8dbdd58589b938e3278bdbda6cc7bf8c38 Author: Jan Zill Date: Fri Mar 20 14:08:37 2026 +1000 make test clearer commit 1b2b6dcd8821a89e1acf5aad50541bcfa17ca3e4 Author: Jan Zill Date: Thu Mar 19 19:55:46 2026 +1000 interaction_sample test to catch index order bug commit fa54204799d4c96d34bc55ec535cc5c7b3805bba Author: Jan Zill Date: Thu Mar 19 16:40:37 2026 +1000 numpy not loop commit 1213b56bb55e38a46c858fc20304d26fb2b3fc0a Author: Jan Zill Date: Thu Mar 19 16:27:35 2026 +1000 reinstate test, up number of draws for comparison commit 5e9847d9582a7be6b453aa72d708532454d8dfd5 Author: Jan Zill Date: Thu Mar 19 16:10:02 2026 +1000 series not array commit 1df7d0d6080f4c6c2d8e57bdabbda49c42c6e2d1 Author: Jan Zill Date: Thu Mar 19 16:02:31 2026 +1000 unify mc and eet reporting during choice making commit 6440d3f9757299103ae4c0588344110a2830bd6d Author: Jan Zill Date: Thu Mar 19 15:34:47 2026 +1000 undo stray comment commit 73f45fecfda2732c5c03112a0d08125f2e0757d4 Author: Jan Zill Date: Thu Mar 19 15:29:59 2026 +1000 reshape, do not flatten for potential performance commit cf92aada878e118c5eaaa9ff212e84112469a698 Author: Tom Stephen Date: Wed Mar 18 14:44:41 2026 +1000 Normalise number of choosers, alternatives and minimum tolerance for EET comparison tests commit df574eeda8517ea6835b2a5e4852878ac5f23b09 Author: Tom Stephen Date: Wed Mar 18 14:35:29 2026 +1000 Linting commit a7f2e8f8daaefdd5ef83d540e0e8d5a6333359b2 Author: Tom Stephen Date: Wed Mar 18 14:30:02 2026 +1000 Fix, complete tests for interaction sample, simulate commit a9db13142f85fd8ae6860ce9243fea75779491f0 Author: Tom Stephen Date: Tue Mar 17 12:04:56 2026 +1000 Add basic with/without EET test for interaction simulate commit 9d69dabb79786101212747055430d4ae6509776f Author: Tom Stephen Date: Mon Mar 16 17:15:28 2026 +1000 add tests, docstrings for logit commit f97185eda08b52ec84bb2167af1f3511a6580c5a Author: Jan Zill Date: Thu Mar 19 15:21:26 2026 +1000 bug fix: order of chooser_idx in interaction_simulate commit 60a744a7aec72884e26b8fc2e3b35d23e2998ccd Author: Jan Zill Date: Thu Mar 19 15:21:26 2026 +1000 no duplicate arrays commit 710b3b0881b1bba3873ab761238bd013fe2ce0a0 Author: Jan Zill Date: Thu Mar 19 15:21:26 2026 +1000 memory reduction commit 5c506f146fa65bfd8e92a122d794dacc7081e65f Author: Jan Zill Date: Thu Mar 19 15:21:26 2026 +1000 np array instead of for loop commit 5604b3e6c2a749333de478d2251f7d44233e24b3 Merge: 6f31c76db 82f1f7d64 Author: Jeffrey Newman Date: Fri Mar 13 08:46:09 2026 -0500 Merge pull request #1042 from driftlesslabs/eet update EET feature branch commit 82f1f7d6477960870a2a9e233db5af27b6f0564f Author: Jeff Newman Date: Thu Mar 12 23:01:52 2026 -0500 update larch pin to 6.0.46 commit 0754bb0c8f637b771050dd5964b925407d666ef6 Merge: 45ab2ca40 b8297d251 Author: Jeff Newman Date: Tue Mar 10 14:22:59 2026 -0500 Merge branch 'main' into eet commit 45ab2ca40632d9d3c1ff78bc680606bcecc3cf75 Author: Jeff Newman Date: Mon Mar 9 14:02:37 2026 -0500 blacken commit 57ba5bc1943fddf40ad78e63000810d1636fb78d Author: Jeff Newman Date: Mon Mar 9 13:50:26 2026 -0500 fix unbound variable in estimation mode commit 7fe87460be2413ae8c1edcdef6edd5cdf3ac7e04 Merge: 0c0f77eac 6f31c76db Author: Jeff Newman Date: Thu Mar 5 12:39:08 2026 -0600 Merge branch 'explicit_error_terms_2' # Conflicts: # activitysim/core/configuration/top.py # activitysim/core/interaction_sample.py commit 6f31c76dbb608468b0dfd86a0253827fca968c8b Author: Jan Zill Date: Fri May 30 13:11:31 2025 +1000 fix allow_zero_probs for prob calculation in EET interaction_sample commit 3b48a076636cd0afc6d1ae19c0f7de6fa4638661 Author: Jan Zill Date: Fri May 30 10:26:11 2025 +1000 lint commit 66cdac906395d70e52ab2970e3fb5af86b59dfcb Author: Jan Zill Date: Fri May 30 10:23:42 2025 +1000 consistently treat utilities that would lead to zero choices in MC simulation commit e102a9e54dbfb9e3dc663c4da06cde840fd42a86 Author: Jan Zill Date: Fri May 30 09:17:47 2025 +1000 adds validate utility method to align with unavailable choices in MC simulation commit 94e629ace077e64fbfa9db74da89c8ca53e7ec03 Author: Jan Zill Date: Thu May 29 22:10:43 2025 +1000 patch other instance of padded_utility for eet commit 5636cd9025a012c2b53891234309949cdb11885d Author: Jan Zill Date: Thu May 29 21:09:25 2025 +1000 lint commit c40b989f717176f227c4705d09d50412899b49dd Author: Jan Zill Date: Thu May 29 20:55:47 2025 +1000 avoid choosing alternatives corresponding to padded utilities in interaction_sample_simulate commit 49fcaa5519e8c5462db2fa92260f6823834f5cd1 Merge: 582c998f5 146c7ffee Author: Jan Zill Date: Thu May 29 14:51:11 2025 +1000 Merge remote-tracking branch 'origin/master' into janzill/utilit_based_choices_2025 commit 582c998f516ab3e26fa888a6de7fd8282d945926 Author: Jan Zill Date: Mon May 26 10:52:14 2025 +1000 comment clean up commit 41d49ffd3d7eff90420708abd6927937e20f456c Author: Jan Zill Date: Mon May 26 10:39:59 2025 +1000 clean up comments commit 1f815e82a78b042da88e85a0cb8daf81896f511f Author: Jan Zill Date: Mon May 26 10:24:21 2025 +1000 remove comment commit ef0be214a901418e6a58c1eb946a5328bcaa7239 Author: Jan Zill Date: Mon May 26 10:06:06 2025 +1000 fix nest_spec is None bug commit 2a563ff1ea2b3e869c87022f17e5bf9455db35bb Author: Jan Zill Date: Mon May 26 08:39:50 2025 +1000 lint commit 5de1d23903677e5c744c95517e2804b768df4717 Author: Jan Zill Date: Mon May 26 08:29:10 2025 +1000 compute settings in disaggreagte accessibility w/o hack commit 28142cdf2cbf63a70ec38b16454c14273659bcb6 Author: Jan Zill Date: Mon May 26 07:59:04 2025 +1000 fix bool conversion of None for eet compute settings in sub-components commit fc686978108f018a433fd9d491cae6d88a443506 Author: Jan Zill Date: Mon May 26 07:40:31 2025 +1000 fix _interaction_sample return doc commit 3793ec24442b9dbf759d8f900b5bb4a684f0a283 Author: Jan Zill Date: Mon May 26 07:36:30 2025 +1000 fix no sampling in interaction_sample commit d6ecb0d9a1b9670fe2122f2a05db3953edd39f3a Author: Jan Zill Date: Mon May 26 06:51:17 2025 +1000 update comments commit 892c3fbe8d2b2f29f1549804f14c5b8c70608496 Author: Jan Zill Date: Mon May 26 06:39:37 2025 +1000 move interaction sample without sampling out of MC loop to enable for both MC and eet commit 9900f48486282d9017109782308396dfe5487026 Author: Jan Zill Date: Sun May 25 19:23:41 2025 +1000 lint commit 0fa183ec26ad8e564b862054a0d3a38a0d285ad8 Author: Jan Zill Date: Sun May 25 11:15:07 2025 +1000 jtp custom chooser for EET commit 9a0fb663dc11e68b288d067aaa5916930f541035 Author: Jan Zill Date: Sun May 25 08:32:05 2025 +1000 more comments commit 9d12b08fd3dbcbb740d5317d6d91a385c160c437 Author: Jan Zill Date: Sun May 25 07:55:04 2025 +1000 comments commit 6f8958be3a0b869156cb64ab673911585d637d42 Author: Jan Zill Date: Sat Apr 12 05:54:06 2025 +1000 lint commit 5637fb0d84ec9c13887ed40534750e300914f096 Author: Jan Zill Date: Sat Apr 12 05:41:00 2025 +1000 test SOA MC for disagg access commit 6a0987d96590f8502353f43c710527b419d716ae Author: Jan Zill Date: Sat Apr 12 05:31:56 2025 +1000 identifies eet todos commit c6d60c6cbf577fc80f0ac6cc73a62d3fb9ff1073 Author: Jan Zill Date: Sat Apr 12 05:23:30 2025 +1000 comment clean up commit 5105b17cbdcf6bf47141a6c5dcd6145086ceaef3 Merge: 30ddb3e19 de6d0ef21 Author: Jan Zill Date: Mon Apr 7 03:38:42 2025 +1000 Merge remote-tracking branch 'origin/master' into janzill/utilit_based_choices_2025 commit 30ddb3e1970d6648d6b9f05e44a11ace2f02a14b Author: Jan Zill Date: Mon Apr 7 02:44:54 2025 +1000 default compute settings set before, no need to check if none commit 11d86a1f53488587c4d7ce9a4fb37ac871fa4b3c Author: Jan Zill Date: Mon Apr 7 02:34:05 2025 +1000 Adds subcomponent-specific eet setting commit 0f54cec22ee73887efd07e2a7c6f17ba5eb64732 Author: Jan Zill Date: Sun Apr 6 20:07:50 2025 +1000 compute_setting overrides for eet, currently only for interaction_sample to test loc sampling with MC commit f5f365fdd8d24be137c255632794c9180bb0797d Author: Jan Zill Date: Wed Apr 2 20:23:57 2025 +1000 removes notebook folder commit 73b8c32471485f2c2d7d88fd03046cef309ad993 Author: Jan Zill Date: Wed Apr 2 20:14:44 2025 +1000 sign on logsum commit 14441a6281faa8ed684bf731ebf95f834ba6f43f Author: Jan Zill Date: Wed Apr 2 16:26:28 2025 +1000 corr commit e1e9d088d706019f9c09639aded311dc31dae8ac Author: Jan Zill Date: Wed Apr 2 15:48:46 2025 +1000 seeding corr test commit 114b747b2e9615a53c7cc453557f7ab07088372d Author: Jan Zill Date: Wed Apr 2 14:37:12 2025 +1000 some variation checks, dev notebooks, to delete commit c6feaefa22ce60dcf4d9557066aa095e61a1a4a5 Author: Jan Zill Date: Sat Mar 29 18:17:34 2025 +1000 log eet setting on start up commit 75bb84d9423134ee5781bd61433db10793e0d035 Author: Jan Zill Date: Sat Mar 29 17:56:50 2025 +1000 more merge fixes, example runs through commit 08d013ab1fcfbdb944062b657e0b68fc38d711aa Author: Jan Zill Date: Sat Mar 29 17:49:10 2025 +1000 more merge bugs commit 792d183eb76010803c477e5c61fead77c71205f2 Author: Jan Zill Date: Sat Mar 29 17:24:25 2025 +1000 merge bug commit b86093fad67369f167f3b0d38073815e101ece67 Merge: 9a1739556 bf64b7beb Author: Jan Zill Date: Sat Mar 29 17:00:53 2025 +1000 Merge branch 'master' into janzill/utilit_based_choices_2025 commit 9a1739556f81847a2057a0e1ec8561573df97c93 Merge: 6b0b4e93e ae7fa438e Author: janzill Date: Tue Jan 31 15:41:45 2023 +1000 resolves merge conflicts with main v1.2 commit 6b0b4e93e575bd5d31dd6930b5b526c74fe91be5 Author: Jan Zill Date: Mon Jun 27 11:13:44 2022 +1000 memory logging commit c0d1ca17ab4fa8d5bd643a97687e21a8af8835dd Author: Jan Zill Date: Fri Jun 24 16:04:36 2022 +1000 gumbel by hand by inverse cdf commit 3973bcde0f590408ab4fdc3587e1b04ebb758ac9 Author: Jan Zill Date: Fri Jun 24 10:34:24 2022 +1000 clean up commit 6451abd8ac1dcc10158d0aa045a84359d1a71f6c Author: Jan Zill Date: Fri Jun 24 10:24:33 2022 +1000 use numpy's gumbel to draw from ev1 commit 313bcd0926bd955a48aa59d046561762812fb60b Author: janzill Date: Tue Jun 21 15:16:51 2022 +1000 remove scale and location params from ev1 quartile fct commit 26a7090218577a95c3b8afd6d220517069fb6372 Author: janzill Date: Tue Jun 21 13:59:47 2022 +1000 interaction sample dev commit d6f4f97531475c472642fb627b80345efec271a4 Author: Jan Zill Date: Tue Jun 21 07:29:51 2022 +1000 remove comment commit 7df4c8ddb4bee084c32a1f0dfc17ad9b949cc1ba Author: Jan Zill Date: Mon Jun 20 23:36:17 2022 +1000 to numpy once commit 7a66a67ded39e284de7fcecc7d6b4f436fff2bb0 Author: Jan Zill Date: Mon Jun 20 23:10:00 2022 +1000 set index commit c6d75d126735ab4e7fb0291def7c3172c100ff2b Author: Jan Zill Date: Mon Jun 20 23:03:21 2022 +1000 delete after last ref, not before commit 78630be674d9b382646f89df2d9045204585e0a3 Author: Jan Zill Date: Mon Jun 20 23:01:39 2022 +1000 spelling commit 2568b72148f9f2c4be4d5bfe61ea45ce4ccdcff8 Author: Jan Zill Date: Mon Jun 20 23:00:59 2022 +1000 memory work commit e0555d439fe2c75265d16f3d78db440130162ec9 Author: Jan Zill Date: Mon Jun 20 22:43:13 2022 +1000 interaction_sample memory saving implementation commit 47601e3bc97e043deb8a172be3c5fd6d520e855d Author: Jan Zill Date: Mon Jun 20 20:18:40 2022 +1000 bug fix commit 2ec41b18f8e8f9d7de378ed1dc5a59f948cd046c Author: Jan Zill Date: Mon Jun 20 20:00:53 2022 +1000 mem tracing in interaction_sample commit 4c6a524e1eacccbc6327c1cc10f3566d6a31ac0d Author: Jan Zill Date: Mon Jun 20 19:47:07 2022 +1000 custom chooser commit dbc809a4ffda797f10bf8b1b377f70ff2a27a4a7 Author: Jan Zill Date: Mon Jun 20 18:05:10 2022 +1000 add utility based choice option for transit virtual pathbuilder commit b4495932ff7122faab31b7f1c11458a2ade7c053 Author: Jan Zill Date: Mon Jun 20 18:02:29 2022 +1000 bug fix commit f6d81d60d1d3035a9f993c76bd8e5af9dd81e2b5 Author: Jan Zill Date: Mon Jun 20 17:57:00 2022 +1000 bug fix commit f0143e99f3bebbe6f29e6e5a07eb37f4df1eb497 Author: Jan Zill Date: Mon Jun 20 17:54:36 2022 +1000 remove false comparison commit 7a6cc29ee70a06aa752e490418401ab6e895a285 Author: Jan Zill Date: Mon Jun 20 17:38:34 2022 +1000 need probs in interaction_sample - try it the quick way commit 4f825ae9c4cbb6980e7c8632030fe9a786554780 Author: Jan Zill Date: Mon Jun 20 17:22:29 2022 +1000 style fixes commit 417e90feb6b95eda77338334455299ead1ebc8e3 Author: Jan Zill Date: Mon Jun 20 17:17:44 2022 +1000 wrong method in cdap commit f0cc563bdb93bd5ca42cf5005c12474a9f952276 Author: Jan Zill Date: Mon Jun 20 17:16:05 2022 +1000 interaction_sample util based w/p probs commit cb807d13edf11ca199f864f8a6481eabaa336862 Author: Jan Zill Date: Mon Jun 20 16:35:49 2022 +1000 remove usage of choose_individual_max_utility commit eea84ca079c1acef4a521372717816b42c7ccba7 Author: Jan Zill Date: Mon Jun 20 16:31:14 2022 +1000 remove usage of choose_individual_max_utility commit 8ac91f19a056c478c7c380423ecffd342c17bc76 Author: Jan Zill Date: Mon Jun 20 16:28:39 2022 +1000 remove usage of choose_individual_max_utility commit 0387c356e604aa75c388a8156bb3b3f9704159cf Author: Jan Zill Date: Mon Jun 20 16:27:33 2022 +1000 remove usage of choose_individual_max_utility commit 6b38f128566027460966ca082c64592f9da46e0f Author: Jan Zill Date: Mon Jun 20 16:26:48 2022 +1000 remove usage of choose_individual_max_utility commit affe2b6dda4c12ec231b67fcca4b27d12d30a078 Author: Jan Zill Date: Mon Jun 20 16:21:32 2022 +1000 remove usage of choose_individual_max_utility commit 17792f5edb81eb0eb64b3f866b99015b3a1b1f2b Author: Jan Zill Date: Mon Jun 20 16:13:49 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit b48dd71bbf9924351dd27659eec925bd3e4c138b Author: Jan Zill Date: Mon Jun 20 16:09:57 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit 4b5cad2681a3ef6a968bfcf0957961d575d8b6f7 Author: Jan Zill Date: Mon Jun 20 16:02:52 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit f9b1e2e09f6d6094aeaf02eb21009afa57ade4e6 Author: Jan Zill Date: Mon Jun 20 15:59:30 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit 043a0bdf1b29cd4c46db9b40e9a30b5b7bb7a84d Author: Jan Zill Date: Mon Jun 20 14:53:12 2022 +1000 working through all make_choices calls to implement memory saving hack until sharrow arrives commit f890799f8ae869acec3b82e18bd3737e6f3142f2 Author: Jan Zill Date: Mon Jun 20 14:36:24 2022 +1000 fake rand tracing commit d14fac389e298e4334fdecd41a14b7642856fc01 Author: Jan Zill Date: Mon Jun 20 14:34:40 2022 +1000 memory saving work - delete dfs wherever possible commit c26c80c08985b547bd8adb8c89473c7218398470 Author: Jan Zill Date: Sun Jun 19 16:58:25 2022 +1000 no fru for probabilitic lookup tables commit 087ec0ebfcd25c4dc4425b87161993dcb2deb638 Author: Jan Zill Date: Sun Jun 19 16:20:34 2022 +1000 use fru in models that use logit.make_choices w/o simulate wrapper commit 8c167658bfc7cc9650271c5b1bb86a5a6f83c2d9 Author: Jan Zill Date: Sun Jun 19 15:56:29 2022 +1000 make choice indexing for nested logit consistent with mnl and cumsum MC commit 61a314f84ffc66600f7f50ecbafe1d6b1a5630ca Author: Jan Zill Date: Sun Jun 19 15:30:07 2022 +1000 move reporting in make choices commit 19d1222b07f962680ce5838b0350a4744d9516c9 Author: Jan Zill Date: Sun Jun 19 15:22:54 2022 +1000 remove debug print statement commit 9685aca3ef75019d4ee75acdbf3383bc4b0e9397 Author: Jan Zill Date: Sun Jun 19 15:06:50 2022 +1000 make place of call consistent commit 8c1d1ed2b2d71c0e3673b115d121df04157ed6a1 Author: Jan Zill Date: Sun Jun 19 14:30:45 2022 +1000 pass arguments in commit 7fbba93275cca185207023ea465a3ebb7caa109a Author: Jan Zill Date: Sun Jun 19 14:03:39 2022 +1000 fies frozen_rand setting call commit bbce6aa028ca1f6cdfbaaea7312fc9bd5ad76ce7 Author: Jan Zill Date: Sun Jun 19 13:52:33 2022 +1000 nested index instead of name commit 005b1311daa1ccf3bbdae6195d5d4d33db4bfe26 Author: Jan Zill Date: Fri Jun 17 10:29:01 2022 +1000 adds interaction_sample at the module level, not individual model commit 00208e7bd3bccb20cec71c9df9cd7f1bb91f3a9b Author: Jan Zill Date: Fri Jun 17 10:24:49 2022 +1000 adds interaction_sample at the module level, not individual model commit e3e4326bdc8c86e426bb8e67a01a3b656e375f68 Author: Jan Zill Date: Fri Jun 17 09:14:45 2022 +1000 interaction sample frozen rand indiv util commit 64c4e6e2c31a3731734487a6b79eba5e114be6e8 Author: Jan Zill Date: Fri Jun 17 08:57:18 2022 +1000 fixes alternative index as chosen value commit 36789a3a2c32f790f3cfafd7a22c2ce155138e56 Author: Jan Zill Date: Thu Jun 16 18:54:04 2022 +1000 interaction_sample dev work commit 9950f846fe56926df8cf9fd167155e38fb104678 Author: Jan Zill Date: Thu Jun 16 13:28:45 2022 +1000 interaction_sample dev work commit e65635d1fb6c033b551e37fadc058142dd3c815b Author: Jan Zill Date: Thu Jun 16 12:55:01 2022 +1000 interaction sample notebook commit 9ac51c91c1438f183daa0736e451efed782416a0 Author: Jan Zill Date: Wed Jun 15 20:22:41 2022 +1000 usage of interaction_sample_simulate commit f3e16107d070ba2c787308eab2a2a42baafd2dc6 Author: Jan Zill Date: Wed Jun 15 19:41:11 2022 +1000 add setting to interaction_simulate call commit 1d4ca9a20ae3c25dbc25c6be76e83ec0cafb86e3 Author: Jan Zill Date: Wed Jun 15 19:39:22 2022 +1000 froxen rand ind util in interaction simulate commit 1a786f26ac8bfcae0855a3a64c44f2bb47cde81c Author: Jan Zill Date: Wed Jun 15 19:33:05 2022 +1000 implement flag in settings, apply to all simple simulate models commit f725ca69093e52d25cfe3d98d0bbf7f5b477af64 Author: Jan Zill Date: Wed Jun 15 19:05:13 2022 +1000 pass through frozen_impl flag commit 0c6f4c8eb5a81505469dab3b6d531a4361682054 Author: Jan Zill Date: Wed Jun 15 19:03:06 2022 +1000 nested now with alternative numbers, not names, change structure to be more consistent with current impl commit 7694563d6574c78920919a1bb99b26addee5cefb Author: Jan Zill Date: Wed Jun 15 15:48:51 2022 +1000 simple siumlate mnl frozen rand ind util commit 921983fb32d8624bffb08615dcd75f70c0ff33c3 Author: Jan Zill Date: Tue May 17 10:53:07 2022 +1000 adds more convergence analysis commit 8668383a489973a91af2c1c5e5043891f05b1409 Author: Jan Zill Date: Tue May 17 00:26:03 2022 +1000 nicer comparison commit 75c16a28d4a9088ac810835efae880b3919ee879 Author: Jan Zill Date: Mon May 16 21:26:02 2022 +1000 validation commit 3700a9e76decb7d4c0582ddf7cba759081a2edda Author: Jan Zill Date: Mon May 16 17:43:32 2022 +1000 some validation metrics commit 42901bf2e7a331529ccad5420e996fbb4c676617 Author: Jan Zill Date: Mon May 16 16:15:51 2022 +1000 adds goodness of fit test and loop over all trips commit 7bfb80a80839a6df6326302a0ed09fd25e223713 Author: Jan Zill Date: Sun May 15 22:32:41 2022 +1000 adds comment regarding potential random choice runtime improvement commit e821b57b9dd6e9cd9596a3c3453cb96b5129482b Author: Jan Zill Date: Tue May 10 14:12:29 2022 +1000 nest scaling comment commit fba872882d5221fc21775f8e6a1cc0a90e59c9b3 Author: Jan Zill Date: Tue May 10 13:10:43 2022 +1000 nest scales in asim seem to be all between 0 and 1, so not like in larch where they are non-increasing - re-adjust util calc. should really document this somewhere commit 4d0d8232aa5ed9b97340c6f683163140e4f19272 Author: Jan Zill Date: Tue May 10 01:10:24 2022 +1000 inspecting a couple of test cases commit 4ee74ea7b9a291ca3ab413f4644698d423f99d2c Author: Jan Zill Date: Tue May 10 00:40:11 2022 +1000 clean up commit e02b46a8c93451edab1c3d5c44bdb6b5258b38e8 Author: Jan Zill Date: Tue May 10 00:35:23 2022 +1000 compare to larch probs commit ebf64c6a238aee78283d4816ab28bf00d27bfc13 Author: Jan Zill Date: Mon May 9 17:23:50 2022 +1000 larch notebook to calculate probs a different way commit 798439208bf923919432f28bf5ae0f59e19412ec Author: Jan Zill Date: Sun May 8 22:45:27 2022 +1000 removes trace in runtime tests commit 6c17d89bace65edff58d5426cbc34a6c8128518f Author: Jan Zill Date: Sun May 8 22:26:14 2022 +1000 caching nest level names and alternatives commit 9f4d2622b09f52e1cf33ac2389a5a04cd8a59e1f Author: Jan Zill Date: Sun May 8 21:32:31 2022 +1000 check commit c95f68df5c08ffd04916fe0ea82c4f52904838f3 Author: Jan Zill Date: Sun May 8 21:15:41 2022 +1000 fixes bug in choice for tree commit 1286183cd2f34dda168baeaac2a6f84d25bc3ed6 Author: Jan Zill Date: Fri May 6 16:57:43 2022 +1000 output clean up commit 932d5adc13776a1f2d7a44381ac46cbe184fa529 Author: Jan Zill Date: Fri May 6 16:41:13 2022 +1000 scaling in recursive nest utilities commit 7c21407cfd2d667a3bf45f7c3f5ea5a830ca3750 Author: Jan Zill Date: Fri May 6 13:27:26 2022 +1000 adds nested utility scale commit b60702b438bc8612eda6a124b61af7d9569f1cca Author: Jan Zill Date: Fri May 6 12:21:50 2022 +1000 clean up commit 4883c3342bcc0766fb4ea9f43b712b86e0ac5044 Author: Jan Zill Date: Fri May 6 12:13:41 2022 +1000 recursive utility calculation fix commit f2cea72263cb23473544ebeedeb51c7be1060f2a Author: Jan Zill Date: Fri May 6 11:59:01 2022 +1000 two level still off commit fd5ca14771973a4929d4116be0c752f98e7b2214 Author: Jan Zill Date: Thu May 5 21:18:50 2022 +1000 fixes scale of nest utilities commit ab8a212977ba886a0a7041f425a3c35580b911b7 Author: Jan Zill Date: Thu May 5 14:11:42 2022 +1000 probs validation clean up commit a31d97f94e9698a37a032b463884f77e3d5cceac Author: Jan Zill Date: Thu May 5 14:08:14 2022 +1000 probs trip set up commit 5e8bf1648342458fcb903c9f0192cec1855543f5 Author: Jan Zill Date: Thu May 5 12:56:34 2022 +1000 towards validation of probs for single trip commit 5a7ad3c76604846057a897ff78c3b7983738776d Author: Jan Zill Date: Wed May 4 23:41:58 2022 +1000 pull out cruft commit 83a86f5575f79922b9fafea6715fc0d342277846 Author: Jan Zill Date: Wed May 4 23:01:29 2022 +1000 scale of leaf utility commit 5dc1a44ed9303e3269207f128a08842954ae49e1 Author: Jan Zill Date: Wed May 4 22:11:12 2022 +1000 baby steps towards validation commit c6b609afd6ffc881964ba560d66b2a12c8248d0f Author: Jan Zill Date: Wed May 4 21:43:07 2022 +1000 prototype simple_simulate commit a4a76309fdbb0c1fd0ae7647a806e1db5724cbff Author: Jan Zill Date: Wed May 4 20:53:30 2022 +1000 adds choice based on walking nest levels commit c973be8130388a7bf061f4d58c079518bcfdcefc Author: Jan Zill Date: Wed May 4 15:52:43 2022 +1000 prototype work - return leaf and node (exp max) utilities, nest spec commit ec8ae759c270add73538a5d79f233b8613a5efe2 Author: Jan Zill Date: Tue May 3 21:10:55 2022 +1000 notebook dev, adds nest coefficient in logit.py Co-authored-by: Jan Co-authored-by: Tom Stephen Co-authored-by: Tyler Pearn --- .github/workflows/core_tests.yml | 1 + .pre-commit-config.yaml | 3 + .../abm/models/disaggregate_accessibility.py | 2 +- .../abm/models/joint_tour_participation.py | 5 +- activitysim/abm/models/location_choice.py | 24 +- .../abm/models/parking_location_choice.py | 8 +- activitysim/abm/models/trip_destination.py | 8 +- .../abm/models/trip_scheduling_choice.py | 9 + activitysim/abm/models/util/test/test_cdap.py | 82 +++ .../abm/models/util/tour_destination.py | 6 + .../models/util/vectorize_tour_scheduling.py | 5 + .../joint_tour_participation.csv | 2 + .../joint_tour_participation.yaml | 5 + .../joint_tour_participation_coefficients.csv | 2 + .../test_joint_tour_participation.py | 158 +++++ .../test_misc/test_trip_departure_choice.py | 398 +++++++---- .../test_misc/test_trip_scheduling_choice.py | 488 +++++++------ activitysim/core/configuration/top.py | 11 + activitysim/core/interaction_sample.py | 75 +- .../core/interaction_sample_simulate.py | 40 +- activitysim/core/logit.py | 298 +++++++- activitysim/core/random.py | 33 +- activitysim/core/simulate.py | 22 +- .../core/test/test_interaction_sample.py | 297 ++++++++ .../test/test_interaction_sample_simulate.py | 255 +++++++ .../core/test/test_interaction_simulate.py | 174 +++++ activitysim/core/test/test_logit.py | 668 +++++++++++++++++- activitysim/core/test/test_simulate.py | 246 ++++++- .../test/configs_eet/settings.yaml | 3 + .../test/reference_pipeline_2_zone_eet.zip | Bin 0 -> 285194 bytes .../test/regress/final_eet_tours_2_zone.csv | 106 +++ .../test/regress/final_eet_trips_2_zone.csv | 254 +++++++ .../test/test_multiple_zone.py | 73 +- .../test/configs_eet/settings.yaml | 5 + .../test/regress/final_eet_trips.csv | 116 +++ .../production_semcog/test/test_semcog.py | 29 +- .../test/configs_eet/settings.yaml | 3 + .../test/regress/final_trips.csv | 182 ++--- .../test/regress/final_trips_eet.csv | 84 +++ .../test/regress/final_trips_sh.csv | 182 ++--- .../examples/prototype_arc/test/test_arc.py | 69 +- docs/core.rst | 14 + docs/dev-guide/explicit-error-terms.md | 188 +++++ docs/dev-guide/index.rst | 1 + docs/users-guide/ways_to_run.rst | 49 +- pyproject.toml | 2 +- uv.lock | 110 +-- 47 files changed, 4037 insertions(+), 758 deletions(-) create mode 100644 activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.csv create mode 100644 activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.yaml create mode 100644 activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation_coefficients.csv create mode 100644 activitysim/abm/test/test_misc/test_joint_tour_participation.py create mode 100644 activitysim/core/test/test_interaction_sample.py create mode 100644 activitysim/core/test/test_interaction_sample_simulate.py create mode 100644 activitysim/core/test/test_interaction_simulate.py create mode 100644 activitysim/examples/placeholder_multiple_zone/test/configs_eet/settings.yaml create mode 100644 activitysim/examples/placeholder_multiple_zone/test/reference_pipeline_2_zone_eet.zip create mode 100644 activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_tours_2_zone.csv create mode 100644 activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_trips_2_zone.csv create mode 100644 activitysim/examples/production_semcog/test/configs_eet/settings.yaml create mode 100644 activitysim/examples/production_semcog/test/regress/final_eet_trips.csv create mode 100644 activitysim/examples/prototype_arc/test/configs_eet/settings.yaml create mode 100644 activitysim/examples/prototype_arc/test/regress/final_trips_eet.csv create mode 100644 docs/dev-guide/explicit-error-terms.md diff --git a/.github/workflows/core_tests.yml b/.github/workflows/core_tests.yml index 8e659b25c1..d8cd9542f6 100644 --- a/.github/workflows/core_tests.yml +++ b/.github/workflows/core_tests.yml @@ -140,6 +140,7 @@ jobs: matrix: region: - prototype_mtc + - prototype_arc - placeholder_psrc - prototype_marin - prototype_mtc_extended diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41fecfab5a..12d970e189 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,9 @@ repos: rev: 22.6.0 hooks: - id: black + language_version: python3.13 + # explicitly set the Python version for the hook environment for black, + # as older versions of black don't work with the latest Python. #- repo: https://github.com/PyCQA/flake8 # rev: 5.0.4 diff --git a/activitysim/abm/models/disaggregate_accessibility.py b/activitysim/abm/models/disaggregate_accessibility.py index e5a66916bd..5fe3375b31 100644 --- a/activitysim/abm/models/disaggregate_accessibility.py +++ b/activitysim/abm/models/disaggregate_accessibility.py @@ -16,9 +16,9 @@ from activitysim.abm.tables import shadow_pricing from activitysim.core import estimation, los, tracing, util, workflow from activitysim.core.configuration.base import ( - ComputeSettings, PreprocessorSettings, PydanticReadable, + ComputeSettings, ) from activitysim.core.configuration.logit import TourLocationComponentSettings from activitysim.core.expressions import assign_columns diff --git a/activitysim/abm/models/joint_tour_participation.py b/activitysim/abm/models/joint_tour_participation.py index 1c8f3af9d0..0fb562c189 100644 --- a/activitysim/abm/models/joint_tour_participation.py +++ b/activitysim/abm/models/joint_tour_participation.py @@ -429,9 +429,8 @@ def joint_tour_participation( if i not in model_settings.compute_settings.protect_columns: model_settings.compute_settings.protect_columns.append(i) - # TODO EET: this is related to the difference in nested logit and logit choice as per comment in - # make_choices_utility_based. As soon as alt_order_array is removed from arguments to - # make_choices_explicit_error_term_nl this guard can be removed + # This is related to the difference in nested logit and logit choice. As soon as alt_order_array + # is removed from arguments to make_choices_explicit_error_term_nl this guard can be removed. if state.settings.use_explicit_error_terms: assert ( nest_spec is None diff --git a/activitysim/abm/models/location_choice.py b/activitysim/abm/models/location_choice.py index 360ebc6b1f..a54805f435 100644 --- a/activitysim/abm/models/location_choice.py +++ b/activitysim/abm/models/location_choice.py @@ -15,11 +15,11 @@ TourLocationComponentSettings, TourModeComponentSettings, ) +from activitysim.core.exceptions import DuplicateWorkflowTableError from activitysim.core.interaction_sample import interaction_sample from activitysim.core.interaction_sample_simulate import interaction_sample_simulate from activitysim.core.logit import AltsContext from activitysim.core.util import reindex -from activitysim.core.exceptions import DuplicateWorkflowTableError """ The school/workplace location model predicts the zones in which various people will @@ -741,6 +741,7 @@ def run_location_choice( chunk_tag, trace_label, skip_choice=False, + alts_context: AltsContext | None = None, ): """ Run the three-part location choice algorithm to generate a location choice for each chooser @@ -760,6 +761,8 @@ def run_location_choice( model_settings : dict chunk_size : int trace_label : str + skip_choice : bool + alts_context : AltsContext or None Returns ------- @@ -792,11 +795,9 @@ def run_location_choice( if choosers.shape[0] == 0: logger.info(f"{trace_label} skipping segment {segment_name}: no choosers") continue - # using land use rather than size terms in case something goes 0 base -> nonzero project, double - # check if that would be in dest_size_terms as a zero - alts_context = AltsContext.from_series(dest_size_terms.index) # index zone_id, not ALT_DEST_COL_NAME - # assumes that dest_size_terms will always contain zeros for non-attractive zones, i.e. it will have the - # same length as land_use + # dest_size_terms contains 0-attraction zones so using this directly here, important for stable error terms + # when a zone goes from 0 base -> nonzero project + alts_context = AltsContext.from_series(dest_size_terms.index) # - location_sample location_sample_df = run_location_sample( @@ -1045,6 +1046,17 @@ def iterate_location_choice( ] persons_merged_df_ = persons_merged_df_.sort_index() + # reset rng offsets to identical state on each iteration. This ensures that the same set of random numbers is + # used on each iteration. Note this has to happen AFTER updating shadow prices because the simulation method + # draws random numbers. + # Only applying when using EET for now because this will need changes to integration + # tests, but it's probably a good idea for MC simulation as well. + if state.settings.use_explicit_error_terms and iteration > 1: + logger.debug( + f"{trace_label} resetting random number generator offsets for iteration {iteration}" + ) + state.get_rn_generator().reset_offsets_for_step(state.current_model_name) + choices_df_, save_sample_df = run_location_choice( state, persons_merged_df_, diff --git a/activitysim/abm/models/parking_location_choice.py b/activitysim/abm/models/parking_location_choice.py index 5df4f3f687..b07ec5b873 100644 --- a/activitysim/abm/models/parking_location_choice.py +++ b/activitysim/abm/models/parking_location_choice.py @@ -214,9 +214,9 @@ def choose_parking_location( ) destination_sample.index = np.repeat(trips.index.values, len(alternatives)) destination_sample.index.name = trips.index.name - # using destination_sample would also be right because destination_sample isn't a sample here, - # but that could change - alts_context = AltsContext.from_series(alternatives[alt_dest_col_name]) + # use full land_use index to ensure AltsContext spans full range of potential zones + land_use = state.get_dataframe("land_use") + alts_context = AltsContext.from_series(land_use.index) destinations = parking_destination_simulate( state, @@ -229,7 +229,7 @@ def choose_parking_location( chunk_size=chunk_size, trace_hh_id=trace_hh_id, trace_label=trace_label, - alts_context=alts_context + alts_context=alts_context, ) if want_sample_table: diff --git a/activitysim/abm/models/trip_destination.py b/activitysim/abm/models/trip_destination.py index 3ad35470cd..df16941489 100644 --- a/activitysim/abm/models/trip_destination.py +++ b/activitysim/abm/models/trip_destination.py @@ -1083,6 +1083,10 @@ def choose_trip_destination( t0 = print_elapsed_time() + # use full index (including zero-size zones) to ensure stable random results + # fetch alts_context early so we don't worry about mutating alternatives first + alts_context = AltsContext.from_series(alternatives.index) + # - trip_destination_sample destination_sample = trip_destination_sample( state, @@ -1129,10 +1133,6 @@ def choose_trip_destination( destination_sample["dp_logsum"] = 0.0 t0 = print_elapsed_time("%s.compute_logsums" % trace_label, t0, debug=True) - alt_dest_col_name = model_settings.ALT_DEST_COL_NAME - alts = alternatives.index - assert alts.name == alt_dest_col_name - alts_context = AltsContext.from_series(alts) destinations = trip_destination_simulate( state, primary_purpose=primary_purpose, diff --git a/activitysim/abm/models/trip_scheduling_choice.py b/activitysim/abm/models/trip_scheduling_choice.py index 254155c333..d2ad92f6cc 100644 --- a/activitysim/abm/models/trip_scheduling_choice.py +++ b/activitysim/abm/models/trip_scheduling_choice.py @@ -279,6 +279,15 @@ def run_trip_scheduling_choice( ) in chunk.adaptive_chunked_choosers(state, indirect_tours, trace_label, explicit_chunk_size=model_settings.explicit_chunk): # Sort the choosers and get the schedule alternatives choosers = choosers.sort_index() + # FIXME-EET: For explicit error term choices, we need a stable alternative ID. Currently, we use + # SCHEDULE_ID, which justs enumerates all schedule alternatives, of which there are choosers times + # alternative, in the order they are processed, which depends on if there stops on outward/return leg. + # We might want to change SCHEDULE_ID to a fixed pattern of all possible combinations of + # (outbound, main, inbound) duration for the maximum possible tour duration (max time window). For + # 30min intervals, this leads to 1225 alternatives and therefore reasonable memory-wise for random numbers. + # It looks like all that would need to change for this is the generation of the schedule alternatives and + # the lookup of choices as elements in schedule after simulation because choosers are indexed by tour_id. + schedules = generate_schedule_alternatives(choosers).sort_index() # Assuming we did the max_alt_size calculation correctly, diff --git a/activitysim/abm/models/util/test/test_cdap.py b/activitysim/abm/models/util/test/test_cdap.py index 20dc6b2410..20d68f2dd9 100644 --- a/activitysim/abm/models/util/test/test_cdap.py +++ b/activitysim/abm/models/util/test/test_cdap.py @@ -5,6 +5,7 @@ import os.path +import numpy as np import pandas as pd import pandas.testing as pdt import pytest @@ -176,3 +177,84 @@ def test_build_cdap_spec_hhsize2(people, model_settings): ).astype("float") pdt.assert_frame_equal(utils, expected, check_names=False) + + +def test_cdap_explicit_error_terms_parity(people, model_settings): + person_type_map = model_settings.get("PERSON_TYPE_MAP", {}) + + # Increase population to get more stable distribution for parity check + # We'll just duplicate the existing people a few times + large_people = pd.concat([people] * 500).reset_index(drop=True) + large_people.index.name = "person_id" + + assert people.household_id.is_monotonic_increasing + large_people["hhid_diff"] = large_people.household_id.diff().fillna(0).astype(int) + large_people.loc[large_people["hhid_diff"] < 0, "hhid_diff"] = 1 + large_people["household_id"] = large_people.hhid_diff.cumsum() + + assert large_people["household_id"].is_monotonic_increasing + + # Run without explicit error terms + state_no_eet = workflow.State.make_default(__file__) + cdap_indiv_spec = state_no_eet.filesystem.read_model_spec( + file_name="cdap_indiv_and_hhsize1.csv" + ) + interaction_coefficients = pd.read_csv( + state_no_eet.filesystem.get_config_file_path( + "cdap_interaction_coefficients.csv" + ), + comment="#", + ) + interaction_coefficients = cdap.preprocess_interaction_coefficients( + interaction_coefficients + ) + cdap_fixed_relative_proportions = pd.DataFrame( + {"activity": ["M", "N", "H"], "coefficient": [0.33, 0.33, 0.34]} + ) + + state_no_eet.settings.use_explicit_error_terms = False + state_no_eet.rng().set_base_seed(42) + state_no_eet.rng().begin_step("test_no_eet") + state_no_eet.rng().add_channel("person_id", large_people) + state_no_eet.rng().add_channel( + "household_id", + large_people.drop_duplicates("household_id").set_index("household_id"), + ) + + choices_no_eet = cdap.run_cdap( + state_no_eet, + large_people, + person_type_map, + cdap_indiv_spec, + interaction_coefficients, + cdap_fixed_relative_proportions, + locals_d=None, + ) + + # Run with explicit error terms + state_eet = workflow.State.make_default(__file__) + state_eet.settings.use_explicit_error_terms = True + state_eet.rng().set_base_seed(42) + state_eet.rng().begin_step("test_eet") + state_eet.rng().add_channel("person_id", large_people) + state_eet.rng().add_channel( + "household_id", + large_people.drop_duplicates("household_id").set_index("household_id"), + ) + + choices_eet = cdap.run_cdap( + state_eet, + large_people, + person_type_map, + cdap_indiv_spec, + interaction_coefficients, + cdap_fixed_relative_proportions, + locals_d=None, + ) + + # Compare distributions + dist_no_eet = choices_no_eet.value_counts(normalize=True).sort_index() + dist_eet = choices_eet.value_counts(normalize=True).sort_index() + + # Check that they are reasonably close + pdt.assert_series_equal(dist_no_eet, dist_eet, atol=0.05, check_names=False) diff --git a/activitysim/abm/models/util/tour_destination.py b/activitysim/abm/models/util/tour_destination.py index d99803bd7d..0531a2caeb 100644 --- a/activitysim/abm/models/util/tour_destination.py +++ b/activitysim/abm/models/util/tour_destination.py @@ -21,6 +21,7 @@ from activitysim.core.configuration.logit import TourLocationComponentSettings from activitysim.core.interaction_sample import interaction_sample from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.logit import AltsContext from activitysim.core.util import reindex logger = logging.getLogger(__name__) @@ -873,6 +874,10 @@ def run_destination_simulate( state.tracing.dump_df(DUMP, choosers, trace_label, "choosers") log_alt_losers = state.settings.log_alt_losers + # use full land_use index to ensure AltsContext spans full range of potential destinations + # (maintains stable random number generation even if zones flip zero/non-zero size) + land_use = state.get_dataframe("land_use") + alts_context = AltsContext.from_series(land_use.index) choices = interaction_sample_simulate( state, @@ -891,6 +896,7 @@ def run_destination_simulate( estimator=estimator, skip_choice=skip_choice, compute_settings=model_settings.compute_settings, + alts_context=alts_context, ) if not want_logsums: diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index c199ef40da..0666bf2c8c 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -17,6 +17,7 @@ from activitysim.core.configuration.base import ComputeSettings, PreprocessorSettings from activitysim.core.configuration.logit import LogitComponentSettings from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.logit import AltsContext from activitysim.core.util import reindex logger = logging.getLogger(__name__) @@ -849,6 +850,9 @@ def _schedule_tours( estimator.write_interaction_sample_alternatives(alt_tdd) log_alt_losers = state.settings.log_alt_losers + # use full TDD alternatives index to ensure AltsContext spans full range of potential slots + tdd_alts = state.get_injectable("tdd_alts") + alts_context = AltsContext.from_series(tdd_alts.index) choices = interaction_sample_simulate( state, @@ -862,6 +866,7 @@ def _schedule_tours( trace_label=tour_trace_label, estimator=estimator, compute_settings=compute_settings, + alts_context=alts_context, ) chunk_sizer.log_df(tour_trace_label, "choices", choices) diff --git a/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.csv b/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.csv new file mode 100644 index 0000000000..d81df1ab13 --- /dev/null +++ b/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.csv @@ -0,0 +1,2 @@ +Description,Expression,participate,not_participate +Adult participation,adult,0.5,-0.5 diff --git a/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.yaml b/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.yaml new file mode 100644 index 0000000000..8db2410c08 --- /dev/null +++ b/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation.yaml @@ -0,0 +1,5 @@ +SPEC: joint_tour_participation.csv +COEFFICIENTS: joint_tour_participation_coefficients.csv +participation_choice: participate +max_participation_choice_iterations: 100 +FORCE_PARTICIPATION: True diff --git a/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation_coefficients.csv b/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation_coefficients.csv new file mode 100644 index 0000000000..237d519172 --- /dev/null +++ b/activitysim/abm/test/test_misc/configs_test_misc/joint_tour_participation_coefficients.csv @@ -0,0 +1,2 @@ +expression,coefficient +adult,1.0 diff --git a/activitysim/abm/test/test_misc/test_joint_tour_participation.py b/activitysim/abm/test/test_misc/test_joint_tour_participation.py new file mode 100644 index 0000000000..5aa15c6e8e --- /dev/null +++ b/activitysim/abm/test/test_misc/test_joint_tour_participation.py @@ -0,0 +1,158 @@ +import numpy as np +import pandas as pd +import pandas.testing as pdt +import pytest + +from activitysim.abm.models import joint_tour_participation +from activitysim.core import logit, workflow + +from .test_trip_departure_choice import add_canonical_dirs + + +@pytest.fixture +def candidates(): + # Create synthetic candidates for Joint Tour Participation + # JTP chooses whether each candidate participates in a joint tour. + # We include varied compositions and preschoolers to exercise the + # get_tour_satisfaction logic properly. + num_tours_per_comp = 500 + compositions = ["MIXED", "ADULTS", "CHILDREN"] + num_candidates_per_tour = 4 + + total_tours = num_tours_per_comp * len(compositions) + num_candidates = total_tours * num_candidates_per_tour + + # Ensure reproducibility + rng = np.random.default_rng(42) + + tour_ids = np.repeat(np.arange(total_tours), num_candidates_per_tour) + comp_values = np.repeat(compositions, num_tours_per_comp * num_candidates_per_tour) + + df = pd.DataFrame( + { + "tour_id": tour_ids, + "household_id": tour_ids, # simplified for mock + "person_id": np.arange(num_candidates), + "composition": comp_values, + }, + index=pd.Index(np.arange(num_candidates), name="participant_id"), + ) + + # Assign adult and preschooler status based on composition + # MIXED: at least one adult and one child + # ADULTS: all adults + # CHILDREN: all children + df["adult"] = False + df["person_is_preschool"] = False + + for i, comp in enumerate(compositions): + mask = df.composition == comp + indices = df[mask].index + + if comp == "ADULTS": + df.loc[indices, "adult"] = True + elif comp == "CHILDREN": + df.loc[indices, "adult"] = False + # Some children are preschoolers + df.loc[ + rng.choice(indices, len(indices) // 4, replace=False), + "person_is_preschool", + ] = True + elif comp == "MIXED": + # For each tour, make the first person an adult, rest children + tour_start_indices = indices[::num_candidates_per_tour] + df.loc[tour_start_indices, "adult"] = True + # Other members are children, some might be preschoolers + other_indices = indices[~indices.isin(tour_start_indices)] + df.loc[ + rng.choice(other_indices, len(other_indices) // 3, replace=False), + "person_is_preschool", + ] = True + + return df + + +@pytest.fixture +def model_spec(): + # Simple spec with two alternatives: 'participate' and 'not_participate' + return pd.DataFrame( + {"participate": [0.8, -0.2], "not_participate": [0.0, 0.0]}, + index=pd.Index(["adult", "person_is_preschool"], name="Expression"), + ) + + +def test_jtp_explicit_error_terms_parity(candidates, model_spec): + """ + Test that joint tour participation results are statistically similar + between MNL and Explicit Error Terms (EET) using realistic candidate scenarios. + """ + # Create random utilities for the candidates that vary by attribute + rng = np.random.default_rng(42) + + # Base utility + some noise + base_util = (candidates.adult * 0.5) - (candidates.person_is_preschool * 1.0) + utils = pd.DataFrame( + { + "participate": base_util + rng.standard_normal(len(candidates)), + "not_participate": 0, + }, + index=candidates.index, + ) + + # Run without EET (MNL) + state_no_eet = add_canonical_dirs("configs_test_misc").default_settings() + state_no_eet.settings.use_explicit_error_terms = False + state_no_eet.rng().set_base_seed(42) + state_no_eet.rng().begin_step("test_no_eet") + state_no_eet.rng().add_channel("participant_id", candidates) + + # MNL path expects probabilities + probs_no_eet = logit.utils_to_probs(state_no_eet, utils, trace_label="test_no_eet") + choices_no_eet, _ = joint_tour_participation.participants_chooser( + state_no_eet, + probs_no_eet, + candidates, + model_spec, + trace_label="test_no_eet", + ) + + # Run with EET + state_eet = add_canonical_dirs("configs_test_misc").default_settings() + state_eet.settings.use_explicit_error_terms = True + state_eet.rng().set_base_seed(42) + state_eet.rng().begin_step("test_eet") + state_eet.rng().add_channel("participant_id", candidates) + + # EET path expects raw utilities + choices_eet, _ = joint_tour_participation.participants_chooser( + state_eet, + utils.copy(), + candidates, + model_spec, + trace_label="test_eet", + ) + + # Compare distributions of number of participants per tour + # Choice 0 is 'participate' + no_eet_participation_counts = ( + (choices_no_eet == 0).groupby(candidates.tour_id).sum() + ) + eet_participation_counts = (choices_eet == 0).groupby(candidates.tour_id).sum() + + dist_no_eet = no_eet_participation_counts.value_counts(normalize=True).sort_index() + dist_eet = eet_participation_counts.value_counts(normalize=True).sort_index() + + # Check that the distribution of participation counts is close + pdt.assert_series_equal(dist_no_eet, dist_eet, atol=0.05, check_names=False) + + # Also check average participation by composition for deeper parity check + comp_parity_no_eet = no_eet_participation_counts.groupby( + candidates.groupby("tour_id")["composition"].first() + ).mean() + comp_parity_eet = eet_participation_counts.groupby( + candidates.groupby("tour_id")["composition"].first() + ).mean() + + pdt.assert_series_equal( + comp_parity_no_eet, comp_parity_eet, atol=0.1, check_names=False + ) diff --git a/activitysim/abm/test/test_misc/test_trip_departure_choice.py b/activitysim/abm/test/test_misc/test_trip_departure_choice.py index 6d462c0bd0..d6645ce94f 100644 --- a/activitysim/abm/test/test_misc/test_trip_departure_choice.py +++ b/activitysim/abm/test/test_misc/test_trip_departure_choice.py @@ -1,152 +1,246 @@ -# import numpy as np -# import pandas as pd -# import pytest -# -# import activitysim.abm.models.trip_departure_choice as tdc -# from activitysim.abm.models.util.trip import get_time_windows -# from activitysim.core import los -# -# from .setup_utils import setup_dirs -# -# -# @pytest.fixture(scope="module") -# def trips(): -# outbound_array = [True, True, False, False, False, True, True, False, False, True] -# -# trips = pd.DataFrame( -# data={ -# "tour_id": [1, 1, 2, 2, 2, 2, 2, 3, 3, 4], -# "trip_duration": [2, 2, 7, 7, 7, 12, 12, 4, 4, 5], -# "inbound_duration": [0, 0, 7, 7, 7, 0, 0, 4, 4, 5], -# "main_leg_duration": [4, 4, 2, 2, 2, 2, 2, 1, 1, 2], -# "outbound_duration": [2, 2, 0, 0, 0, 12, 12, 0, 0, 5], -# "trip_count": [2, 2, 3, 3, 3, 2, 2, 2, 2, 1], -# "trip_num": [1, 2, 1, 2, 3, 1, 2, 1, 2, 1], -# "outbound": outbound_array, -# "chunk_id": [1, 1, 2, 2, 2, 2, 2, 3, 3, 4], -# "is_work": [ -# True, -# True, -# False, -# False, -# False, -# False, -# False, -# False, -# False, -# True, -# ], -# "is_school": [ -# False, -# False, -# False, -# False, -# False, -# False, -# False, -# True, -# True, -# False, -# ], -# "is_eatout": [ -# False, -# False, -# True, -# True, -# True, -# True, -# True, -# False, -# False, -# False, -# ], -# "start": [8, 8, 18, 18, 18, 18, 18, 24, 24, 19], -# "end": [14, 14, 39, 39, 39, 39, 39, 29, 29, 26], -# "origin": [3, 5, 15, 12, 24, 8, 17, 8, 9, 6], -# "destination": [5, 9, 12, 24, 20, 17, 18, 9, 11, 14], -# }, -# index=range(10), -# ) -# -# trips.index.name = "trip_id" -# return trips -# -# -# @pytest.fixture(scope="module") -# def settings(): -# return { -# "skims_file": "skims.omx", -# "skim_time_periods": {"labels": ["EA", "AM", "MD", "PM", "NT"]}, -# } -# -# -# @pytest.fixture(scope="module") -# def model_spec(): -# index = [ -# "@(df['stop_time_duration'] * df['is_work'].astype(int)).astype(int)", -# "@(df['stop_time_duration'] * df['is_school'].astype(int)).astype(int)", -# "@(df['stop_time_duration'] * df['is_eatout'].astype(int)).astype(int)", -# ] -# -# values = { -# "inbound": [0.933020, 0.370260, 0.994840], -# "outbound": [0.933020, 0.370260, 0.994840], -# } -# -# return pd.DataFrame(index=index, data=values) -# -# -# def test_build_patterns(trips): -# time_windows = get_time_windows(48, 3) -# patterns = tdc.build_patterns(trips, time_windows) -# patterns = patterns.sort_values(["tour_id", "outbound", "trip_num"]) -# -# assert patterns.shape[0] == 34 -# assert patterns.shape[1] == 6 -# assert patterns.index.name == tdc.TOUR_LEG_ID -# -# output_columns = [ -# tdc.TOUR_ID, -# tdc.PATTERN_ID, -# tdc.TRIP_NUM, -# tdc.STOP_TIME_DURATION, -# tdc.TOUR_ID, -# tdc.OUTBOUND, -# ] -# -# assert set(output_columns).issubset(patterns.columns) -# -# -# def test_get_tour_legs(trips): -# tour_legs = tdc.get_tour_legs(trips) -# assert tour_legs.index.name == tdc.TOUR_LEG_ID -# assert ( -# np.unique(tour_legs[tdc.TOUR_ID].values).shape[0] -# == np.unique(trips[tdc.TOUR_ID].values).shape[0] -# ) -# -# -# def test_generate_alternative(trips): -# alts = tdc.generate_alternatives(trips, tdc.STOP_TIME_DURATION) -# assert alts.shape[0] == 67 -# assert alts.shape[1] == 1 -# -# assert alts.index.name == tdc.TRIP_ID -# assert alts.columns[0] == tdc.STOP_TIME_DURATION -# -# pd.testing.assert_series_equal( -# trips.groupby(trips.index)["trip_duration"].max(), -# alts.groupby(alts.index)[tdc.STOP_TIME_DURATION].max(), -# check_names=False, -# ) -# -# -# def test_apply_stage_two_model(state, model_spec, trips): -# setup_dirs() -# departures = tdc.apply_stage_two_model( -# state, model_spec, trips, 0, "TEST Trip Departure" -# ) -# assert len(departures) == len(trips) -# pd.testing.assert_index_equal(departures.index, trips.index) -# -# departures = pd.concat([trips, departures], axis=1) +import numpy as np +import pandas as pd +import pytest +import os + +import activitysim.abm.models.trip_departure_choice as tdc +from activitysim.abm.models.util.trip import get_time_windows +from activitysim.core import workflow + +from .setup_utils import setup_dirs + + +@pytest.fixture(scope="module") +def trips(): + trips = pd.DataFrame( + data={ + "tour_id": [1, 1, 2, 2, 2, 2, 2, 3, 3, 4], + "trip_duration": [2, 2, 7, 7, 7, 12, 12, 4, 4, 5], + "inbound_duration": [0, 0, 7, 7, 7, 0, 0, 4, 4, 5], + "main_leg_duration": [4, 4, 2, 2, 2, 2, 2, 1, 1, 2], + "outbound_duration": [2, 2, 0, 0, 0, 12, 12, 0, 0, 5], + "trip_count": [2, 2, 3, 3, 3, 2, 2, 2, 2, 1], + "trip_num": [1, 2, 1, 2, 3, 1, 2, 1, 2, 1], + "outbound": [ + True, + True, + False, + False, + False, + True, + True, + False, + False, + True, + ], + "chunk_id": [1, 1, 2, 2, 2, 2, 2, 3, 3, 4], + "is_work": [ + True, + True, + False, + False, + False, + False, + False, + False, + False, + True, + ], + "is_school": [ + False, + False, + False, + False, + False, + False, + False, + True, + True, + False, + ], + "is_eatout": [ + False, + False, + True, + True, + True, + True, + True, + False, + False, + False, + ], + "start": [8, 8, 18, 18, 18, 18, 18, 24, 24, 19], + "end": [14, 14, 39, 39, 39, 39, 39, 29, 29, 26], + "origin": [3, 5, 15, 12, 24, 8, 17, 8, 9, 6], + "destination": [5, 9, 12, 24, 20, 17, 18, 9, 11, 14], + }, + index=range(10), + ) + + trips.index.name = "trip_id" + return trips + + +@pytest.fixture(scope="module") +def settings(): + return { + "skims_file": "skims.omx", + "skim_time_periods": {"labels": ["EA", "AM", "MD", "PM", "NT"]}, + } + + +def add_canonical_dirs(configs_dir_name): + state = workflow.State() + configs_dir = os.path.join(os.path.dirname(__file__), f"{configs_dir_name}") + data_dir = os.path.join(os.path.dirname(__file__), f"data") + output_dir = os.path.join(os.path.dirname(__file__), "output") + state.initialize_filesystem( + working_dir=os.path.dirname(__file__), + configs_dir=(configs_dir,), + output_dir=output_dir, + data_dir=(data_dir,), + ) + return state + + +@pytest.fixture(scope="module") +def model_spec(): + index = [ + "@(df['stop_time_duration'] * df['is_work'].astype(int)).astype(int)", + "@(df['stop_time_duration'] * df['is_school'].astype(int)).astype(int)", + "@(df['stop_time_duration'] * df['is_eatout'].astype(int)).astype(int)", + ] + + values = { + "inbound": [0.933020, 0.370260, 0.994840], + "outbound": [0.933020, 0.370260, 0.994840], + } + + return pd.DataFrame(index=index, data=values) + + +def test_build_patterns(trips): + time_windows = get_time_windows(48, 3) + patterns = tdc.build_patterns(trips, time_windows) + patterns = patterns.sort_values(["tour_id", "outbound", "trip_num"]) + + assert patterns.shape[0] == 34 + assert patterns.shape[1] == 6 + assert patterns.index.name == tdc.TOUR_LEG_ID + + output_columns = [ + tdc.TOUR_ID, + tdc.PATTERN_ID, + tdc.TRIP_NUM, + tdc.STOP_TIME_DURATION, + tdc.TOUR_ID, + tdc.OUTBOUND, + ] + + assert set(output_columns).issubset(patterns.columns) + + +def test_get_tour_legs(trips): + tour_legs = tdc.get_tour_legs(trips) + assert tour_legs.index.name == tdc.TOUR_LEG_ID + assert ( + np.unique(tour_legs[tdc.TOUR_ID].values).shape[0] + == np.unique(trips[tdc.TOUR_ID].values).shape[0] + ) + + +def test_generate_alternative(trips): + alts = tdc.generate_alternatives(trips, tdc.STOP_TIME_DURATION) + assert alts.shape[0] == 67 + assert alts.shape[1] == 1 + + assert alts.index.name == tdc.TRIP_ID + assert alts.columns[0] == tdc.STOP_TIME_DURATION + + pd.testing.assert_series_equal( + trips.groupby(trips.index)["trip_duration"].max(), + alts.groupby(alts.index)[tdc.STOP_TIME_DURATION].max(), + check_names=False, + ) + + +def test_apply_stage_two_model(model_spec, trips): + setup_dirs() + state = add_canonical_dirs("configs_test_misc").default_settings() + + # A settings object is needed to pass to the model application function, + # but for testing we can just use the default settings. + # In non-testing use cases, the SPEC would actually be read from the yaml file + # instead of being passed directly as a dataframe. + model_settings = tdc.TripDepartureChoiceSettings() + + departures = tdc.apply_stage_two_model( + state, + model_spec, + trips, + 0, + "TEST Trip Departure", + model_settings=model_settings, + ) + assert len(departures) == len(trips) + pd.testing.assert_index_equal(departures.index, trips.index) + + departures = pd.concat([trips, departures], axis=1) + + +def test_tdc_explicit_error_terms_parity(model_spec, trips): + setup_dirs() + model_settings = tdc.TripDepartureChoiceSettings() + + # Increase population for statistical convergence + large_trips = pd.concat([trips] * 500).reset_index(drop=True) + large_trips.index.name = "trip_id" + # Ensure tour_ids are distinct for the expanded set + large_trips["tour_id"] = ( + large_trips.groupby("tour_id").cumcount() * 1000 + large_trips["tour_id"] + ) + + # Trip departure choice uses tour_leg_id as the random channel index + tour_legs = tdc.get_tour_legs(large_trips) + + # Run without explicit error terms + state_no_eet = add_canonical_dirs("configs_test_misc").default_settings() + state_no_eet.settings.use_explicit_error_terms = False + state_no_eet.rng().set_base_seed(42) + state_no_eet.rng().begin_step("test_no_eet") + state_no_eet.rng().add_channel("trip_id", large_trips) + state_no_eet.rng().add_channel("tour_leg_id", tour_legs) + + departures_no_eet = tdc.apply_stage_two_model( + state_no_eet, + model_spec, + large_trips, + 0, + "TEST Trip Departure No EET", + model_settings=model_settings, + ) + + # Run with explicit error terms + state_eet = add_canonical_dirs("configs_test_misc").default_settings() + state_eet.settings.use_explicit_error_terms = True + state_eet.rng().set_base_seed(42) + state_eet.rng().begin_step("test_eet") + state_eet.rng().add_channel("trip_id", large_trips) + state_eet.rng().add_channel("tour_leg_id", tour_legs) + + departures_eet = tdc.apply_stage_two_model( + state_eet, + model_spec, + large_trips, + 0, + "TEST Trip Departure EET", + model_settings=model_settings, + ) + + # Compare distributions + dist_no_eet = departures_no_eet.value_counts(normalize=True).sort_index() + dist_eet = departures_eet.value_counts(normalize=True).sort_index() + + # Check that they are reasonably close (within 5% for this sample size) + pd.testing.assert_series_equal(dist_no_eet, dist_eet, atol=0.05, check_names=False) diff --git a/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py b/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py index 24fdebde3e..8401c785c1 100644 --- a/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py +++ b/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py @@ -1,199 +1,289 @@ -# import numpy as np -# import pandas as pd -# import pytest -# -# from activitysim.abm.models import trip_scheduling_choice as tsc -# from activitysim.abm.tables.skims import skim_dict -# from activitysim.core import los, workflow -# -# from .setup_utils import setup_dirs -# -# -# @pytest.fixture(scope="module") -# def tours(): -# tours = pd.DataFrame( -# data={ -# "duration": [2, 44, 32, 12, 11, 16], -# "num_outbound_stops": [2, 4, 0, 0, 1, 3], -# "num_inbound_stops": [1, 0, 0, 2, 1, 2], -# "tour_type": ["othdisc"] * 2 + ["eatout"] * 4, -# "origin": [3, 10, 15, 23, 5, 8], -# "destination": [5, 9, 12, 24, 20, 17], -# tsc.LAST_OB_STOP: [1, 3, 0, 0, 12, 14], -# tsc.FIRST_IB_STOP: [2, 0, 0, 4, 6, 20], -# }, -# index=range(6), -# ) -# -# tours.index.name = "tour_id" -# -# tours[tsc.HAS_OB_STOPS] = tours[tsc.NUM_OB_STOPS] >= 1 -# tours[tsc.HAS_IB_STOPS] = tours[tsc.NUM_IB_STOPS] >= 1 -# -# return tours -# -# -# @pytest.fixture(scope="module") -# def settings(): -# return {"skims_file": "skims.omx", "skim_time_periods": {"labels": ["MD"]}} -# -# -# @pytest.fixture(scope="module") -# def model_spec(): -# index = [ -# "@(df['main_leg_duration']>df['duration']).astype(int)", -# "@(df['main_leg_duration'] == 0)&(df['tour_type']=='othdiscr')", -# "@(df['main_leg_duration'] == 1)&(df['tour_type']=='othdiscr')", -# "@(df['main_leg_duration'] == 2)&(df['tour_type']=='othdiscr')", -# "@(df['main_leg_duration'] == 3)&(df['tour_type']=='othdiscr')", -# "@(df['main_leg_duration'] == 4)&(df['tour_type']=='othdiscr')", -# "@df['tour_type']=='othdiscr'", -# "@df['tour_type']=='eatout'", -# "@df['tour_type']=='eatout'", -# ] -# -# values = [ -# -999, -# -6.5884, -# -5.0326, -# -2.0526, -# -1.0313, -# -0.46489, -# 0.060382, -# -0.7508, -# 0.53247, -# ] -# -# return pd.DataFrame(index=index, data=values, columns=["stage_one"]) -# -# -# @pytest.fixture(scope="module") -# def skims(settings): -# setup_dirs() -# nw_los = los.Network_LOS() -# nw_los.load_data() -# skim_d = skim_dict(nw_los) -# -# od_skim_stack_wrapper = skim_d.wrap("origin", "destination") -# do_skim_stack_wrapper = skim_d.wrap("destination", "origin") -# obib_skim_stack_wrapper = skim_d.wrap(tsc.LAST_OB_STOP, tsc.FIRST_IB_STOP) -# -# skims = [od_skim_stack_wrapper, do_skim_stack_wrapper, obib_skim_stack_wrapper] -# -# return skims -# -# -# @pytest.fixture(scope="module") -# def locals_dict(skims): -# return {"od_skims": skims[0], "do_skims": skims[1], "obib_skims": skims[2]} -# -# -# def test_generate_schedule_alternatives(tours): -# windows = tsc.generate_schedule_alternatives(tours) -# assert windows.shape[0] == 296 -# assert windows.shape[1] == 4 -# -# output_columns = [ -# tsc.SCHEDULE_ID, -# tsc.MAIN_LEG_DURATION, -# tsc.OB_DURATION, -# tsc.IB_DURATION, -# ] -# -# assert set(output_columns).issubset(windows.columns) -# -# -# def test_no_stops_patterns(tours): -# no_stops = tours[ -# (tours["num_outbound_stops"] == 0) & (tours["num_inbound_stops"] == 0) -# ].copy() -# windows = tsc.no_stops_patterns(no_stops) -# -# assert windows.shape[0] == 1 -# assert windows.shape[1] == 3 -# -# output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] -# -# assert set(output_columns).issubset(windows.columns) -# -# pd.testing.assert_series_equal( -# windows[tsc.MAIN_LEG_DURATION], -# no_stops["duration"], -# check_names=False, -# check_dtype=False, -# ) -# assert windows[windows[tsc.IB_DURATION] > 0].empty -# assert windows[windows[tsc.OB_DURATION] > 0].empty -# -# -# def test_one_way_stop_patterns(tours): -# one_way_stops = tours[ -# ( -# (tours["num_outbound_stops"] > 0).astype(int) -# + (tours["num_inbound_stops"] > 0).astype(int) -# ) -# == 1 -# ].copy() -# windows = tsc.stop_one_way_only_patterns(one_way_stops) -# -# assert windows.shape[0] == 58 -# assert windows.shape[1] == 3 -# -# output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] -# -# assert set(output_columns).issubset(windows.columns) -# -# inbound_options = windows[(windows[tsc.IB_DURATION] > 0)] -# outbound_options = windows[windows[tsc.OB_DURATION] > 0] -# assert np.unique(inbound_options.index).shape[0] == 1 -# assert np.unique(outbound_options.index).shape[0] == 1 -# -# -# def test_two_way_stop_patterns(tours): -# two_way_stops = tours[ -# ( -# (tours["num_outbound_stops"] > 0).astype(int) -# + (tours["num_inbound_stops"] > 0).astype(int) -# ) -# == 2 -# ].copy() -# windows = tsc.stop_two_way_only_patterns(two_way_stops) -# -# assert windows.shape[0] == 237 -# assert windows.shape[1] == 3 -# -# output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] -# -# assert set(output_columns).issubset(windows.columns) -# -# -# def test_run_trip_scheduling_choice( -# state: workflow.State, model_spec, tours, skims, locals_dict -# ): -# """ -# Test run the model. -# """ -# -# out_tours = tsc.run_trip_scheduling_choice( -# state, -# model_spec, -# tours, -# skims, -# locals_dict, -# trace_label="PyTest Trip Scheduling", -# ) -# -# assert len(tours) == len(out_tours) -# pd.testing.assert_index_equal( -# tours.sort_index().index, out_tours.sort_index().index -# ) -# -# output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] -# -# assert set(output_columns).issubset(out_tours.columns) -# -# assert len( -# out_tours[ -# out_tours[output_columns].sum(axis=1) == out_tours[tsc.TOUR_DURATION_COLUMN] -# ] -# ) == len(tours) +from __future__ import annotations + +import os +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from activitysim.abm.models import trip_scheduling_choice as tsc +from activitysim.abm.tables.skims import skim_dict +from activitysim.core import los, workflow + + +@pytest.fixture(scope="module") +def tours(): + tours = pd.DataFrame( + data={ + "duration": [2, 44, 32, 12, 11, 16], + "num_outbound_stops": [2, 4, 0, 0, 1, 3], + "num_inbound_stops": [1, 0, 0, 2, 1, 2], + "tour_type": ["othdisc"] * 2 + ["eatout"] * 4, + "origin": [3, 10, 15, 23, 5, 8], + "destination": [5, 9, 12, 24, 20, 17], + tsc.LAST_OB_STOP: [1, 3, 0, 0, 12, 14], + tsc.FIRST_IB_STOP: [2, 0, 0, 4, 6, 20], + }, + index=range(6), + ) + + tours.index.name = "tour_id" + + tours[tsc.HAS_OB_STOPS] = tours[tsc.NUM_OB_STOPS] >= 1 + tours[tsc.HAS_IB_STOPS] = tours[tsc.NUM_IB_STOPS] >= 1 + + return tours + + +@pytest.fixture(scope="module") +def model_spec(): + index = [ + "@(df['main_leg_duration']>df['duration']).astype(int)", + "@(df['main_leg_duration'] == 0)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 1)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 2)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 3)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 4)&(df['tour_type']=='othdiscr')", + "@df['tour_type']=='othdiscr'", + "@df['tour_type']=='eatout'", + "@df['tour_type']=='eatout'", + ] + + values = [ + -999, + -6.5884, + -5.0326, + -2.0526, + -1.0313, + -0.46489, + 0.060382, + -0.7508, + 0.53247, + ] + + return pd.DataFrame(index=index, data=values, columns=["stage_one"]).rename_axis( + "Expression" + ) + + +def add_canonical_dirs(configs_dir_name): + state = workflow.State() + configs_dir = os.path.join(os.path.dirname(__file__), f"{configs_dir_name}") + data_dir = os.path.join(os.path.dirname(__file__), "data") + output_dir = os.path.join(os.path.dirname(__file__), "output") + state.initialize_filesystem( + working_dir=os.path.dirname(__file__), + configs_dir=(configs_dir,), + output_dir=output_dir, + data_dir=(data_dir,), + ) + return state + + +@pytest.fixture(scope="module") +def skims(): + state = add_canonical_dirs("configs_test_misc").default_settings() + nw_los = los.Network_LOS(state, los_settings_file_name="settings_60_min.yaml") + nw_los.load_data() + skim_d = skim_dict(state, nw_los) + + od_skim_stack_wrapper = skim_d.wrap("origin", "destination") + do_skim_stack_wrapper = skim_d.wrap("destination", "origin") + obib_skim_stack_wrapper = skim_d.wrap(tsc.LAST_OB_STOP, tsc.FIRST_IB_STOP) + + skims = [od_skim_stack_wrapper, do_skim_stack_wrapper, obib_skim_stack_wrapper] + + return skims + + +@pytest.fixture(scope="module") +def locals_dict(skims): + return {"od_skims": skims[0], "do_skims": skims[1], "obib_skims": skims[2]} + + +@pytest.fixture(scope="module") +def base_dir() -> Path: + """ + A pytest fixture that returns the data folder location. + :return: folder location for any necessary data to initialize the tests + """ + return Path(__file__).parent + + +@pytest.fixture(scope="module") +def module() -> str: + """ + A pytest fixture that returns the module name string used in test setup. + :return: module name string ("summarize") + """ + return "summarize" + + +# Used by conftest.py initialize_pipeline method +@pytest.fixture(scope="module") +def tables() -> dict[str, str]: + """ + A pytest fixture that returns the "mock" tables to build pipeline dataframes. The + key-value pair is the name of the table and the index column. + :return: dict + """ + return { + "land_use": "zone_id", + "tours": "tour_id", + "trips": "trip_id", + "persons": "person_id", + "households": "household_id", + } + + +# Used by conftest.py initialize_pipeline method +# Set to true if you need to read skims into the pipeline +@pytest.fixture(scope="module") +def initialize_network_los() -> bool: + """ + A pytest boolean fixture indicating whether network skims should be read from the + fixtures test data folder. + :return: bool + """ + return True + + +def test_generate_schedule_alternatives(tours): + windows = tsc.generate_schedule_alternatives(tours) + assert windows.shape[0] == 296 + assert windows.shape[1] == 4 + + output_columns = [ + tsc.SCHEDULE_ID, + tsc.MAIN_LEG_DURATION, + tsc.OB_DURATION, + tsc.IB_DURATION, + ] + + assert set(output_columns).issubset(windows.columns) + + +def test_no_stops_patterns(tours): + no_stops = tours[ + (tours["num_outbound_stops"] == 0) & (tours["num_inbound_stops"] == 0) + ].copy() + windows = tsc.no_stops_patterns(no_stops) + + assert windows.shape[0] == 1 + assert windows.shape[1] == 3 + + output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + pd.testing.assert_series_equal( + windows[tsc.MAIN_LEG_DURATION], + no_stops["duration"], + check_names=False, + check_dtype=False, + ) + assert windows[windows[tsc.IB_DURATION] > 0].empty + assert windows[windows[tsc.OB_DURATION] > 0].empty + + +def test_one_way_stop_patterns(tours): + one_way_stops = tours[ + ( + (tours["num_outbound_stops"] > 0).astype(int) + + (tours["num_inbound_stops"] > 0).astype(int) + ) + == 1 + ].copy() + windows = tsc.stop_one_way_only_patterns(one_way_stops) + + assert windows.shape[0] == 58 + assert windows.shape[1] == 3 + + output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + inbound_options = windows[(windows[tsc.IB_DURATION] > 0)] + outbound_options = windows[windows[tsc.OB_DURATION] > 0] + assert np.unique(inbound_options.index).shape[0] == 1 + assert np.unique(outbound_options.index).shape[0] == 1 + + +def test_two_way_stop_patterns(tours): + two_way_stops = tours[ + ( + (tours["num_outbound_stops"] > 0).astype(int) + + (tours["num_inbound_stops"] > 0).astype(int) + ) + == 2 + ].copy() + windows = tsc.stop_two_way_only_patterns(two_way_stops) + + assert windows.shape[0] == 237 + assert windows.shape[1] == 3 + + output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + +def test_run_trip_scheduling_choice(model_spec, tours, skims, locals_dict): + # create a temporary workflow state with no content + state = workflow.State.make_temp() + + # Define model settings for this test. + # The settings for this model requires a filename for the spec, but in this test we + # are passing the spec dataframe directly, so the filename is just a placeholder. + # In non-testing use cases, the SPEC would actually be read from the yaml file + # instead of being passed directly as a dataframe. + model_settings = tsc.TripSchedulingChoiceSettings( + **{ + "SPEC": "placeholder.csv", + "compute_settings": { + "protect_columns": ["origin", "destination", "schedule_id"] + }, + } + ) + + # As is common in ActivitySim the component will modify the input dataframe in-place. + # For testing we make a copy of the input tours to compare against after running the model. + in_tours = tours.copy(deep=True) + + # run the trip scheduling choice model + out_tours = tsc.run_trip_scheduling_choice( + state, + model_spec, + tours, + skims, + locals_dict, + trace_label="PyTest Trip Scheduling", + model_settings=model_settings, + ) + + # check that the number of tours is unchanged + assert len(in_tours) == len(out_tours) + pd.testing.assert_index_equal( + in_tours.sort_index().index, out_tours.sort_index().index + ) + + # check that the expected output columns are not present in input tours + output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] + for col in output_columns: + assert col not in in_tours.columns + + # check that the expected output columns *are* present in output tours + assert set(output_columns).issubset(out_tours.columns) + + # check that the sum of the output durations equals the tour duration + assert len( + out_tours[ + out_tours[output_columns].sum(axis=1) == out_tours[tsc.TOUR_DURATION_COLUMN] + ] + ) == len(in_tours) + + # check that tours with no outbound stops have zero outbound duration + assert out_tours[tsc.OB_DURATION].mask(in_tours[tsc.HAS_OB_STOPS], 0).sum() == 0 + + # check that tours with no inbound stops have zero inbound duration + assert out_tours[tsc.IB_DURATION].mask(in_tours[tsc.HAS_IB_STOPS], 0).sum() == 0 diff --git a/activitysim/core/configuration/top.py b/activitysim/core/configuration/top.py index bded7123d9..002d55e845 100644 --- a/activitysim/core/configuration/top.py +++ b/activitysim/core/configuration/top.py @@ -775,6 +775,17 @@ def _check_store_skims_in_shm(self): .. versionadded:: 1.3 """ + use_explicit_error_terms: bool = False + """ + Make choice from random utility model by drawing from distribution of unobserved + part of utility and taking the maximum of total utility. + + Defaults to standard Monte Carlo method, i.e., calculating probabilities and then + drawing a single uniform random number to draw from cumulative probabily. + + .. versionadded:: 1.6 + """ + check_model_settings: bool = True """ run checks to validate that YAML settings files are loadable and spec and coefficent csv can be resolved. diff --git a/activitysim/core/interaction_sample.py b/activitysim/core/interaction_sample.py index e66840778a..51fe835bcb 100644 --- a/activitysim/core/interaction_sample.py +++ b/activitysim/core/interaction_sample.py @@ -54,8 +54,8 @@ def make_sample_choices_utility_based( alternative_count, alt_col_name, allow_zero_probs, - trace_label:str, - chunk_sizer:ChunkSizer, + trace_label: str, + chunk_sizer: ChunkSizer, ): assert isinstance(utilities, pd.DataFrame) assert utilities.shape == (len(choosers), alternative_count) @@ -68,11 +68,9 @@ def make_sample_choices_utility_based( utilities.sum(axis=1) <= utilities.shape[1] * logit.UTIL_UNAVAILABLE ) if zero_probs.all(): - utils = pd.DataFrame( + return pd.DataFrame( columns=[alt_col_name, "rand", "prob", choosers.index.name] ) - probs = pd.DataFrame(0.0, index=utils.index, columns=utils.columns) - return utils, probs if zero_probs.any(): # remove from sample utilities = utilities[~zero_probs] @@ -108,7 +106,7 @@ def make_sample_choices_utility_based( # and the sampling correction factor np.log(df.pick_count/df.prob) is applied to the simulate utilities. # TODO is it safe change the meaning of df.prob, given it's referenced in expression csvs? # (but the alternative is to update all the expression CSV for sampling?) - return choices_df, inclusion_probs + return choices_df def _poisson_sample_alternatives(alternative_count, chunk_sizer: ChunkSizer, probs: pd.DataFrame, sample_size, @@ -610,6 +608,12 @@ def _interaction_sample( return choices_df if use_eet: + + if estimation.manager.enabled: + raise ValueError( + "cannot use explicit error terms with estimation mode at this time" + ) + utilities = logit.validate_utils( state, utilities, @@ -618,7 +622,7 @@ def _interaction_sample( trace_choosers=choosers, ) - choices_df, probs = make_sample_choices_utility_based( + choices_df = make_sample_choices_utility_based( state, choosers, utilities, @@ -630,25 +634,8 @@ def _interaction_sample( trace_label=trace_label, chunk_sizer=chunk_sizer, ) - del utilities chunk_sizer.log_df(trace_label, "utilities", None) - - if estimation.manager.enabled and sample_size > 0: - - choices_df = _ensure_chosen_alts_in_sample( - alt_col_name, - alternatives, - choices_df, - choosers, - probs, - state, - trace_label, - ) - - del probs - chunk_sizer.log_df(trace_label, "probs", None) - else: # convert to probabilities (utilities exponentiated and normalized to probs) # probs is same shape as utilities, one row per chooser and one column for alternative @@ -688,15 +675,39 @@ def _interaction_sample( chunk_sizer.log_df(trace_label, "choices_df", choices_df) if estimation.manager.enabled and sample_size > 0: - choices_df = _ensure_chosen_alts_in_sample( - alt_col_name, - alternatives, - choices_df, - choosers, - probs, - state, - trace_label, + # we need to ensure chosen alternative is included in the sample + survey_choices = estimation.manager.get_survey_destination_choices( + state, choosers, trace_label ) + if survey_choices is not None: + assert ( + survey_choices.index == choosers.index + ).all(), "survey_choices and choosers must have the same index" + survey_choices.name = alt_col_name + survey_choices = survey_choices.dropna().astype( + choices_df[alt_col_name].dtype + ) + + # merge all survey choices onto choices_df + probs_df = probs.reset_index().melt( + id_vars=[choosers.index.name], + var_name=alt_col_name, + value_name="prob", + ) + # probs are numbered 0..n-1 so we need to map back to alt ids + zone_map = pd.Series(alternatives.index).to_dict() + probs_df[alt_col_name] = probs_df[alt_col_name].map(zone_map) + + survey_choices = pd.merge( + survey_choices, + probs_df, + on=[choosers.index.name, alt_col_name], + how="left", + ) + survey_choices["rand"] = 0 + survey_choices["prob"].fillna(0, inplace=True) + choices_df = pd.concat([choices_df, survey_choices], ignore_index=True) + choices_df.sort_values(by=[choosers.index.name], inplace=True) del probs chunk_sizer.log_df(trace_label, "probs", None) diff --git a/activitysim/core/interaction_sample_simulate.py b/activitysim/core/interaction_sample_simulate.py index 2dbd52e871..d9a3c24960 100644 --- a/activitysim/core/interaction_sample_simulate.py +++ b/activitysim/core/interaction_sample_simulate.py @@ -265,13 +265,14 @@ def _interaction_sample_simulate( padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999) padded_alt_nrs = np.insert(interaction_df[choice_column], inserts, -999) chunk_sizer.log_df(trace_label, "padded_utilities", padded_utilities) - del inserts + del interaction_df del interaction_utilities - # TODO EET: chunk_sizer logging could be more precise chunk_sizer.log_df(trace_label, "interaction_df", None) chunk_sizer.log_df(trace_label, "interaction_utilities", None) + del inserts + # reshape to array with one row per chooser, one column per alternative padded_utilities = padded_utilities.reshape(-1, max_sample_count) padded_alt_nrs = padded_alt_nrs.reshape(-1, max_sample_count) @@ -286,6 +287,13 @@ def _interaction_sample_simulate( alt_nrs_df = None # if we don't provide the number of dense alternatives, assume that we'll use the old approach chunk_sizer.log_df(trace_label, "utilities_df", utilities_df) + # alt_nrs_df has columns for each alt in the choice set, with values indicating which alt_id + # they correspond to (as opposed to the 0-n index implied by the column number). + if alts_context is not None: + alt_nrs_df = pd.DataFrame(padded_alt_nrs, index=choosers.index) + else: + alt_nrs_df = None # if we don't provide the number of dense alternatives, assume that we'll use the old approach + del padded_utilities del padded_alt_nrs chunk_sizer.log_df(trace_label, "padded_utilities", None) @@ -330,8 +338,12 @@ def _interaction_sample_simulate( # positions is series with the chosen alternative represented as a column index in utilities_df # which is an integer between zero and num alternatives in the alternative sample positions, rands = logit.make_choices_utility_based( - state, utilities_df, trace_label=trace_label, trace_choosers=choosers, alts_context=alts_context, - alt_nrs_df=alt_nrs_df + state, + utilities_df, + trace_label=trace_label, + trace_choosers=choosers, + alts_context=alts_context, + alt_nrs_df=alt_nrs_df, ) del utilities_df @@ -508,12 +520,12 @@ def interaction_sample_simulate( explicit_chunk_size : float, optional If > 0, specifies the chunk size to use when chunking the interaction simulation. If < 1, specifies the fraction of the total number of choosers. - alts_context: int, optional - The number of alternatives available in the choice set in the absense of sampling. + alts_context: AltsContext, optional + Representation of the full alternatives domain (min and max alternative id) + in the absence of sampling. This is used with EET simulation to ensure consistent random numbers across the whole alternative set ( as the sampled set may change between base and project). When not provided, - the fallback approach is used which may result in frozen error terms being applied to the wrong alternatives - if the choice set changes. + EET with integer-coded choice ids will raise an error. Returns ------- @@ -535,6 +547,18 @@ def interaction_sample_simulate( trace_label = tracing.extend_trace_label(trace_label, "interaction_sample_simulate") chunk_tag = chunk_tag or trace_label + if state.settings.use_explicit_error_terms: + choice_ids_are_int = pd.api.types.is_integer_dtype(alternatives[choice_column]) + if alts_context is None and choice_ids_are_int: + raise ValueError( + "alts_context is required for interaction_sample_simulate when " + "use_explicit_error_terms is True and choice_column is integer-coded" + ) + if alts_context is not None and not choice_ids_are_int: + raise ValueError( + "alts_context can only be used with integer-coded choice_column values" + ) + result_list = [] for ( i, diff --git a/activitysim/core/logit.py b/activitysim/core/logit.py index 8ab8ee1cf3..41e2b795a3 100644 --- a/activitysim/core/logit.py +++ b/activitysim/core/logit.py @@ -24,7 +24,6 @@ EXP_UTIL_MIN = 1e-300 EXP_UTIL_MAX = np.inf -# TODO-EET: Figure out what type we want UTIL_MIN to be, currently np.float64 UTIL_MIN = np.log(EXP_UTIL_MIN, dtype=np.float64) UTIL_UNAVAILABLE = 1000.0 * (UTIL_MIN - 1.0) @@ -33,6 +32,38 @@ PROB_MAX = 1.0 +@dataclass +class AltsContext: + """Representation of the alternatives without carrying around that full array.""" + + min_alt_id: int + max_alt_id: int + + def __post_init__(self): + # e.g. for zero based zones max_alt_id = n_alts - 1 + # but for 1 based zones, we don't need to add extra padding + self.n_rands_to_sample = max(self.max_alt_id, self.n_alts_to_cover_max_id) + + @classmethod + def from_series(cls, ser: Union[pd.Series, pd.Index]) -> "AltsContext": + min_alt_id = ser.min() + max_alt_id = ser.max() + return cls(min_alt_id, max_alt_id) + + @classmethod + def from_num_alts(cls, num_alts: int, zero_based: bool = True) -> "AltsContext": + if zero_based: + offset = -1 + else: + offset = 0 + return cls(min_alt_id=1 + offset, max_alt_id=num_alts + offset) + + @property + def n_alts_to_cover_max_id(self) -> int: + """If zones were non-consecutive, this could be a big over-estimate.""" + return self.max_alt_id + 1 + + def report_bad_choices( state: workflow.State, bad_row_map, @@ -511,6 +542,271 @@ def make_choices_utility_based( return choices, rands +def add_ev1_random( + state: workflow.State, + df: pd.DataFrame, + alt_info: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +): + """ + Add iid EV1 (Gumbel) random error terms to utilities for EET choice. + + Parameters + ---------- + state : workflow.State + df : pandas.DataFrame + Utilities indexed by chooser and with alternatives as columns. + + Returns + ------- + pandas.DataFrame + Utilities with EV1 errors added. + """ + nest_utils_for_choice = df.copy() + assert (alt_info is None) == ( + alt_nrs_df is None + ), "alt_info and alt_nrs_df must both be provided or omitted together" + + if alt_info is None: + # Fallback behaviour for models where alt_info/alt_nrs_df are not provided (e.g. non-integer alts) + rands = state.get_rn_generator().gumbel_for_df( + nest_utils_for_choice, n=nest_utils_for_choice.shape[1] + ) + nest_utils_for_choice += rands + return nest_utils_for_choice + + idx_array = alt_nrs_df.values + mask = idx_array == -999 + safe_idx = np.where(mask, 1, idx_array) # replace -999 with a temp value inbounds + # generate random number for all alts - this is wasteful, but ensures that the same zone + # gets the same random number if the sampled choice set changes between base and project + # (alternatively, one could seed a channel for (persons x zones) and use the zone seed to ensure consistency. + # Trade off is needing to seed (persons x zones) rows and multiindex channels to + # avoid extra random numbers generated here. Quick benchmark suggests seeding per row is likely slower + rands_dense = state.get_rn_generator().gumbel_for_df( + nest_utils_for_choice, n=alt_info.n_alts_to_cover_max_id + ) + # generate n=alt_info.max_alt_id+1 rather than n_alts so that indexing works + # (this is drawing a random number for a redundant zeroth zone in 1 based zoning systems) + # TODO deal with non 0->n-1 indexed land use more efficiently? ideally do where alt_nrs_df is constructed, + # not on the fly here. Potentially via state.get_injectable('network_los').get_skim_dict('taz').zone_ids + rands = np.take_along_axis(rands_dense, safe_idx, axis=1) + rands[ + mask + ] = 0 # zero out the masked zones so they don't have the util adjustment of alt 0 + + nest_utils_for_choice += rands + return nest_utils_for_choice + + +def choose_from_tree( + nest_utils, all_alternatives, logit_nest_groups, nest_alternatives_by_name +): + for level, nest_names in logit_nest_groups.items(): + if level == 1: + next_level_alts = nest_alternatives_by_name[nest_names[0]] + continue + choice_this_level = nest_utils[nest_utils.index.isin(next_level_alts)].idxmax() + if choice_this_level in all_alternatives: + return choice_this_level + next_level_alts = nest_alternatives_by_name[choice_this_level] + raise ValueError("This should never happen - no alternative found") + + +def make_choices_explicit_error_term_nl( + state, + nested_utilities, + alt_order_array, + nest_spec, + trace_label, + trace_choosers=None, + allow_bad_utils=False, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +): + """ + Walk down the nesting tree and make a choice at each level using EET. + + Parameters + ---------- + state : workflow.State + nested_utilities : pandas.DataFrame + Utilities for nest and leaf nodes. + alt_order_array : numpy.ndarray + Leaf alternatives in the original ordering. + nest_spec : dict or LogitNestSpec + Nest specification for the choice model. + trace_label : str + Trace label for logging and tracing. + + Returns + ------- + pandas.Series + Choice indices aligned to `alt_order_array`. + """ + if trace_label: + state.tracing.trace_df( + nested_utilities, tracing.extend_trace_label(trace_label, "nested_utils") + ) + nest_utils_for_choice = add_ev1_random( + state, nested_utilities, alts_context, alt_nrs_df + ) + + all_alternatives = set(nest.name for nest in each_nest(nest_spec, type="leaf")) + logit_nest_groups = group_nest_names_by_level(nest_spec) + nest_alternatives_by_name = {n.name: n.alternatives for n in each_nest(nest_spec)} + + # Apply is slow. It could *maybe* be sped up by using the fact that the nesting structure is the same for all rows: + # Add ev1(0,1) to all entries (as is currently being done). Then, at each level, pick the maximum of the available + # composite alternatives and set the corresponding entry to 1 for each row, set all other alternatives at this level + # to zero. Once the tree is walked (all alternatives have been processed), take the product of the alternatives in + # each leaf's alternative list. Then pick the only alternative with entry 1, all others must be 0. + choices = nest_utils_for_choice.apply( + lambda x: choose_from_tree( + x, all_alternatives, logit_nest_groups, nest_alternatives_by_name + ), + axis=1, + ) + missing_choices = choices.isnull() # TODO: should we check for infs here too? + if missing_choices.any() and not allow_bad_utils: + report_bad_choices( + state, + missing_choices, + nested_utilities, + trace_label=tracing.extend_trace_label(trace_label, "bad_utils"), + msg="no alternative selected", + # raise_error=False, + trace_choosers=trace_choosers, + ) + choices = pd.Series(choices, index=nest_utils_for_choice.index) + + # In order for choice indexing to be consistent with MNL and cumsum MC choices, we need to index in the order + # alternatives were originally created before adding nest nodes that are not elemental alternatives + choices = choices.map({v: k for k, v in enumerate(alt_order_array)}) + + return choices + + +def make_choices_explicit_error_term_mnl( + state, + utilities, + trace_label, + trace_choosers=None, + allow_bad_utils=False, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +) -> pd.Series: + """ + Make EET choices for a multinomial logit model by adding EV1 errors. + + Parameters + ---------- + state : workflow.State + utilities : pandas.DataFrame + Utilities with choosers as rows and alternatives as columns. + trace_label : str + Trace label for logging and tracing. + + Returns + ------- + pandas.Series + Choice indices aligned to the utilities columns order. + """ + if trace_label: + state.tracing.trace_df( + utilities, tracing.extend_trace_label(trace_label, "utilities") + ) + utilities_incl_unobs = add_ev1_random(state, utilities, alts_context, alt_nrs_df) + if trace_label: + state.tracing.trace_df( + utilities_incl_unobs, + tracing.extend_trace_label(trace_label, "utilities_eet"), + ) + choices = np.argmax(utilities_incl_unobs.to_numpy(), axis=1) + missing_choices = np.isnan(choices) # TODO: should we check for infs here too? + if missing_choices.any() and not allow_bad_utils: + report_bad_choices( + state, + missing_choices, + utilities, + trace_label=tracing.extend_trace_label(trace_label, "bad_utils"), + msg="no alternative selected", + # raise_error=False, + trace_choosers=trace_choosers, + ) + choices = pd.Series(choices, index=utilities_incl_unobs.index) + return choices + + +def make_choices_explicit_error_term( + state, + utilities, + alt_order_array, + nest_spec=None, + trace_label=None, + trace_choosers=None, + allow_bad_utils=False, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +) -> pd.Series: + trace_label = tracing.extend_trace_label(trace_label, "make_choices_eet") + if nest_spec is None: + choices = make_choices_explicit_error_term_mnl( + state, + utilities, + trace_label, + trace_choosers, + allow_bad_utils, + alts_context, + alt_nrs_df, + ) + else: + choices = make_choices_explicit_error_term_nl( + state, + utilities, + alt_order_array, + nest_spec, + trace_label, + trace_choosers, + allow_bad_utils, + alts_context, + alt_nrs_df, + ) + return choices + + +def make_choices_utility_based( + state: workflow.State, + utilities: pd.DataFrame, + name_mapping=None, + nest_spec=None, + trace_label: str = None, + trace_choosers=None, + allow_bad_utils=False, + alts_context: AltsContext | None = None, + alt_nrs_df: pd.DataFrame | None = None, +) -> tuple[pd.Series, pd.Series]: + trace_label = tracing.extend_trace_label(trace_label, "make_choices_utility_based") + + # For nested models, choices are mapped to `name_mapping` ordering inside the + # EET helper. For MNL, choices already follow the utilities column order. + choices = make_choices_explicit_error_term( + state, + utilities, + name_mapping, + nest_spec, + trace_label, + trace_choosers=trace_choosers, + allow_bad_utils=allow_bad_utils, + alts_context=alts_context, + alt_nrs_df=alt_nrs_df, + ) + # EET does not expose per-row random draws; return zeros for compatibility. + rands = pd.Series(np.zeros_like(utilities.index.values), index=utilities.index) + + return choices, rands + + def make_choices( state: workflow.State, probs: pd.DataFrame, diff --git a/activitysim/core/random.py b/activitysim/core/random.py index 79e6f7e7a4..ea42b24118 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -445,7 +445,38 @@ def get_channel_for_df(self, df): raise TableIndexError("No channel with index name '%s'" % df.index.name) return self.channels[channel_name] - # step handling + def reset_offsets_for_step(self, step_name): + """ + Reset offsets for all channels for a step + + Parameters + ---------- + step_name : str + pipeline step name for this step + """ + + assert self.step_name == step_name + + for c in self.channels: + self.channels[c].row_states["offset"] = 0 + + def reset_offsets_for_df(self, df): + """ + Reset offsets for all choosers in df if the channel for a step + + Parameters + ---------- + step_name : str + pipeline step name for this step + df : pandas.DataFrame + df with index name and values corresponding to a registered channel + """ + channel = self.get_channel_for_df(df) + channel.row_states.loc[df.index, "offset"] = 0 + logger.info( + f"RNG: resetting random number generator offsets for channel '{channel.channel_name}' for {len(df)} rows" + + f" with index name '{df.index.name}'. Total lenght df: {len(channel.row_states)}" + ) def begin_step(self, step_name): """ diff --git a/activitysim/core/simulate.py b/activitysim/core/simulate.py index bf9c23f5d7..1d7fe6e505 100644 --- a/activitysim/core/simulate.py +++ b/activitysim/core/simulate.py @@ -10,7 +10,7 @@ from collections.abc import Callable from datetime import timedelta from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np import pandas as pd @@ -34,7 +34,8 @@ TemplatedLogitComponentSettings, ) -from activitysim.core.exceptions import ModelConfigurationError +if TYPE_CHECKING: + from activitysim.core.estimation import Estimator from activitysim.core.fast_eval import fast_eval from activitysim.core.simulate_consts import ( ALT_LOSER_UTIL, @@ -1494,7 +1495,6 @@ def eval_nl( ) if state.settings.use_explicit_error_terms: - # TODO-EET: Nested utility zero choice probability raw_utilities = logit.validate_utils( state, raw_utilities, allow_zero_probs=True, trace_label=trace_label ) @@ -1503,21 +1503,13 @@ def eval_nl( nested_utilities = compute_nested_utilities(raw_utilities, nest_spec) chunk_sizer.log_df(trace_label, "nested_utilities", nested_utilities) - # TODO-EET: use nested_utiltites directly to compute logsums? if want_logsums: - # logsum of nest root - # exponentiated utilities of leaves and nests - nested_exp_utilities = compute_nested_exp_utilities( - raw_utilities, nest_spec - ) - chunk_sizer.log_df( - trace_label, "nested_exp_utilities", nested_exp_utilities - ) - logsums = pd.Series(np.log(nested_exp_utilities.root), index=choosers.index) + logsums = pd.Series(nested_utilities.root, index=choosers.index) chunk_sizer.log_df(trace_label, "logsums", logsums) - # TODO-EET: index of choices for nested utilities is different than unnested - this needs to be consistent for - # turning indexes into alternative names to keep code changes to minimum for now + # Index of choices for nested utilities is different than unnested - this needs to be consistent for + # turning indexes into alternative names to keep code changes to minimum for now. Might want to look + # into changing this in the future when revisiting nested logit EET code. name_mapping = raw_utilities.columns.values del raw_utilities diff --git a/activitysim/core/test/test_interaction_sample.py b/activitysim/core/test/test_interaction_sample.py new file mode 100644 index 0000000000..623b1622fb --- /dev/null +++ b/activitysim/core/test/test_interaction_sample.py @@ -0,0 +1,297 @@ +# ActivitySim +# See full license in LICENSE.txt. + +import numpy as np +import pandas as pd +import pytest + +from activitysim.core import interaction_sample, workflow + + +@pytest.fixture +def state() -> workflow.State: + state = workflow.State().default_settings() + state.settings.check_for_variability = False + return state + + +def test_interaction_sample_parity(state): + # Run interaction_sample with and without explicit error terms and check that results are similar. + + num_choosers = 100_000 + num_alts = 100 + sample_size = 10 + + # Create random choosers and alternatives + rng = np.random.default_rng(42) + choosers = pd.DataFrame( + {"chooser_attr": rng.random(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + alternatives = pd.DataFrame( + {"alt_attr": rng.random(num_alts)}, + index=pd.Index(range(num_alts), name="alt_id"), + ) + + # Simple spec: utility = chooser_attr * alt_attr + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["chooser_attr * alt_attr"], name="Expression"), + ) + + # Run _without_ explicit error terms + state.settings.use_explicit_error_terms = False + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_mnl") + + choices_mnl = interaction_sample.interaction_sample( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + alt_col_name="alt_id", + ) + + # Run _with_ explicit error terms + state.init_state() # reset the state to rerun with same seed + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_explicit") + + choices_explicit = interaction_sample.interaction_sample( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + alt_col_name="alt_id", + ) + + assert "alt_id" in choices_mnl.columns + assert "alt_id" in choices_explicit.columns + assert not choices_mnl["alt_id"].isna().any() + assert not choices_explicit["alt_id"].isna().any() + assert choices_mnl["alt_id"].isin(alternatives.index).all() + assert choices_explicit["alt_id"].isin(alternatives.index).all() + + # In interaction_sample, choices_explicit and choices_mnl are DataFrames with sampled alternatives. + # The statistics of chosen alternatives should be similar. + mnl_counts = choices_mnl["alt_id"].value_counts(normalize=True).sort_index() + explicit_counts = ( + choices_explicit["alt_id"].value_counts(normalize=True).sort_index() + ) + + # Check top choices overlap significantly or shares are close + all_alts = set(mnl_counts.index) | set(explicit_counts.index) + for alt in all_alts: + share_mnl = mnl_counts.get(alt, 0) + share_explicit = explicit_counts.get(alt, 0) + diff = abs(share_mnl - share_explicit) + assert diff < 0.01, ( + f"Large discrepancy at alt {alt}: " + f"mnl={share_mnl:.4f}, explicit={share_explicit:.4f}, diff={diff:.4f}" + ) + + +def test_interaction_sample_eet_unavailable_alternatives(state): + # Test that EET handles unavailable alternatives in sampling + num_choosers = 100 + num_alts = 10 + sample_size = 2 + choosers = pd.DataFrame( + {"chooser_attr": np.ones(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + # Alt 0-4 are attractive, Alt 5-9 are "unavailable" + alternatives = pd.DataFrame( + {"alt_attr": [10.0] * 5 + [-1000.0] * 5}, + index=pd.Index(range(num_alts), name="alt_id"), + ) + + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + # Run with EET + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_unavailable_eet") + + choices_eet = interaction_sample.interaction_sample( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + alt_col_name="alt_id", + ) + + # Sampled alternatives should only be from Alt 0-4 + assert choices_eet["alt_id"].isin([0, 1, 2, 3, 4]).all() + assert not choices_eet["alt_id"].isin([5, 6, 7, 8, 9]).any() + + +def test_interaction_sample_parity_peaked_utilities(state): + # Stress parity under a highly peaked utility profile: + # one dominant alternative, one secondary, and many tiny utilities. + num_choosers = 20_000 + num_alts = 100 + sample_size = 5 + + choosers = pd.DataFrame( + {"chooser_attr": np.ones(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + alt_utils = np.array([10.0, 1.0] + [0.0] * (num_alts - 2), dtype=np.float64) + alternatives = pd.DataFrame( + {"alt_attr": alt_utils}, + index=pd.Index(range(num_alts), name="alt_id"), + ) + + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + # Run non-EET path. + state.settings.use_explicit_error_terms = False + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_peaked_mnl") + choices_mnl = interaction_sample.interaction_sample( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + alt_col_name="alt_id", + ) + + # Run EET path with the same seed. + state.init_state() + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_peaked_explicit") + choices_explicit = interaction_sample.interaction_sample( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + alt_col_name="alt_id", + ) + + def weighted_shares(df: pd.DataFrame) -> pd.Series: + counts = df.groupby("alt_id")["pick_count"].sum() + return (counts / counts.sum()).sort_index() + + mnl_shares = weighted_shares(choices_mnl) + explicit_shares = weighted_shares(choices_explicit) + + all_alts = set(mnl_shares.index) | set(explicit_shares.index) + for alt in all_alts: + diff = abs(mnl_shares.get(alt, 0.0) - explicit_shares.get(alt, 0.0)) + assert diff < 0.005, ( + f"Peaked utility parity mismatch at alt {alt}: " + f"mnl={mnl_shares.get(alt, 0.0):.6f}, " + f"explicit={explicit_shares.get(alt, 0.0):.6f}, diff={diff:.6f}" + ) + + # The dominant alternative should absorb almost all mass in both paths. + assert mnl_shares.get(0, 0.0) > 0.99 + assert explicit_shares.get(0, 0.0) > 0.99 + + +class _DummyChunkSizer: + def log_df(self, *_args, **_kwargs): + return None + + +class _DummyState: + def __init__(self, rng): + self._rng = rng + + def get_rn_generator(self): + return self._rng + + +class _DummyRngUtilityBased: + def __init__(self, rands_3d): + self.rands_3d = rands_3d + + def gumbel_for_df(self, _utilities, n): + assert n == self.rands_3d.shape[1] * self.rands_3d.shape[2] + return self.rands_3d.reshape(-1) + + +def test_make_sample_choices_utility_based_repeat_alignment_chooser_dominant_heterogeneity(): + # Edge case: utilities are close across alternatives but vary strongly by chooser. + # This is where wrong chooser/sample alignment can hide in aggregate checks. + chooser_index = pd.Index([101, 102, 103, 104, 105, 106], name="person_id") + choosers = pd.DataFrame(index=chooser_index) + alternatives = pd.DataFrame(index=pd.Index([0, 1, 2, 3], name="alt_id")) + + n_choosers = len(choosers) + n_alts = len(alternatives) + sample_size = 3 + + # Very small alternative differences... + alt_signal = np.array([0.00, 0.01, 0.02, 0.03], dtype=np.float64) + # ...but very large chooser sensitivity differences. + chooser_scale = np.array([-500.0, -200.0, -50.0, 50.0, 200.0, 500.0]) + + utilities = pd.DataFrame( + chooser_scale[:, np.newaxis] * alt_signal[np.newaxis, :], + index=chooser_index, + ) + + # No random noise: chosen alternative is deterministic argmax of utilities. + rands_3d = np.zeros((n_choosers, n_alts, sample_size), dtype=np.float64) + state = _DummyState(_DummyRngUtilityBased(rands_3d)) + + out = interaction_sample.make_sample_choices_utility_based( + state=state, + choosers=choosers, + utilities=utilities, + alternatives=alternatives, + sample_size=sample_size, + alternative_count=n_alts, + alt_col_name="alt_id", + allow_zero_probs=False, + trace_label="test_repeat_alignment_chooser_heterogeneity", + chunk_sizer=_DummyChunkSizer(), + ) + + # Reconstruct expected indexing behavior. + chosen_2d = np.argmax( + rands_3d + utilities.to_numpy()[:, :, np.newaxis], + axis=1, + ) + chosen_flat = chosen_2d.reshape(-1) + + chooser_repeat = np.repeat(np.arange(n_choosers), sample_size) + chooser_tile = np.tile(np.arange(n_choosers), sample_size) + + probs = interaction_sample.logit.utils_to_probs( + state, + utilities, + allow_zero_probs=False, + trace_label="test_repeat_alignment_chooser_heterogeneity", + overflow_protection=True, + trace_choosers=choosers, + ).to_numpy() + + expected_prob_repeat = probs[chooser_repeat, chosen_flat] + wrong_prob_tile = probs[chooser_tile, chosen_flat] + + assert np.array_equal(out["prob"].to_numpy(), expected_prob_repeat) + assert not np.array_equal(out["prob"].to_numpy(), wrong_prob_tile) diff --git a/activitysim/core/test/test_interaction_sample_simulate.py b/activitysim/core/test/test_interaction_sample_simulate.py new file mode 100644 index 0000000000..62a40825f8 --- /dev/null +++ b/activitysim/core/test/test_interaction_sample_simulate.py @@ -0,0 +1,255 @@ +# ActivitySim +# See full license in LICENSE.txt. + +import numpy as np +import pandas as pd +import pytest + +from activitysim.core import interaction_sample_simulate, workflow +from activitysim.core.logit import AltsContext + + +@pytest.fixture +def state() -> workflow.State: + state = workflow.State().default_settings() + state.settings.check_for_variability = False + return state + + +def test_interaction_sample_simulate_parity(state): + # Run interaction_sample_simulate with and without explicit error terms and check that results are similar. + + num_choosers = 100_000 + num_alts_per_chooser = 5 # small sample size to keep things simple + + # Create random choosers + rng = np.random.default_rng(42) + choosers = pd.DataFrame( + {"chooser_attr": rng.random(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + # Create random alternatives for each chooser + # In interaction_sample_simulate, alternatives is typically a DataFrame with the same index as choosers + # but repeated for each alternative in the sample. + alt_ids = np.tile(np.arange(num_alts_per_chooser), num_choosers) + alternatives = pd.DataFrame( + { + "alt_attr": rng.random(num_choosers * num_alts_per_chooser), + "alt_id": alt_ids, + "tdd": alt_ids, + }, + index=np.repeat(choosers.index, num_alts_per_chooser), + ) + alternatives.index.name = "person_id" + + # Simple spec: utility = chooser_attr * alt_attr + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["chooser_attr * alt_attr"], name="Expression"), + ) + + # Run _without_ explicit error terms + state.settings.use_explicit_error_terms = False + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_mnl") + + choices_mnl = interaction_sample_simulate.interaction_sample_simulate( + state, + choosers, + alternatives, + spec, + choice_column="tdd", + ) + + # Run _with_ explicit error terms + state.init_state() + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_explicit") + + choices_explicit = interaction_sample_simulate.interaction_sample_simulate( + state, + choosers, + alternatives, + spec, + choice_column="tdd", + alts_context=AltsContext.from_num_alts(num_alts_per_chooser, zero_based=True), + ) + + assert len(choices_mnl) == num_choosers + assert len(choices_explicit) == num_choosers + assert choices_mnl.index.equals(choosers.index) + assert choices_explicit.index.equals(choosers.index) + assert not choices_mnl.isna().any() + assert not choices_explicit.isna().any() + + # choices are series with the same index as choosers and containing the choice (from choice_column) + mnl_counts = choices_mnl.value_counts(normalize=True).sort_index() + explicit_counts = choices_explicit.value_counts(normalize=True).sort_index() + + for alt in range(num_alts_per_chooser): + share_mnl = mnl_counts.get(alt, 0) + share_explicit = explicit_counts.get(alt, 0) + diff = abs(share_mnl - share_explicit) + assert diff < 0.01, ( + f"Large discrepancy at alt {alt}: " + f"mnl={share_mnl:.4f}, explicit={share_explicit:.4f}, diff={diff:.4f}" + ) + + +def test_interaction_sample_simulate_eet_unavailable_alternatives(state): + # Test that EET handles unavailable alternatives in sample simulation + + num_choosers = 10 + num_alts_per_chooser = 5 + + choosers = pd.DataFrame( + {"chooser_attr": np.ones(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + # For each chooser, 2 attractive alts, 3 unavailable + alt_attrs = [10.0, 10.0, -1000.0, -1000.0, -1000.0] * num_choosers + alt_ids = [0, 1, 2, 3, 4] * num_choosers + + alternatives = pd.DataFrame( + { + "alt_attr": alt_attrs, + "alt_id": alt_ids, + "tdd": alt_ids, + }, + index=np.repeat(choosers.index, num_alts_per_chooser), + ) + alternatives.index.name = "person_id" + + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + # Run with EET + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_unavailable_eet") + + choices_eet = interaction_sample_simulate.interaction_sample_simulate( + state, + choosers, + alternatives, + spec, + choice_column="tdd", + alts_context=AltsContext.from_num_alts(num_alts_per_chooser, zero_based=True), + ) + + assert len(choices_eet) == num_choosers + assert choices_eet.index.equals(choosers.index) + assert not choices_eet.isna().any() + + # Choices should only be 0 or 1 + assert choices_eet.isin([0, 1]).all() + assert not choices_eet.isin([2, 3, 4]).any() + + +def test_interaction_sample_simulate_passes_alts_context_and_alt_nrs_df( + state, monkeypatch +): + state.settings.use_explicit_error_terms = True + + choosers = pd.DataFrame( + {"chooser_attr": [1.0, 1.0]}, + index=pd.Index([100, 101], name="person_id"), + ) + alternatives = pd.DataFrame( + { + "alt_attr": [1.0, 0.5, 0.8, 1.2], + "tdd": [0, 2, 0, 2], + }, + index=pd.Index([100, 100, 101, 101], name="person_id"), + ) + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + captured = {} + + def fake_make_choices_utility_based( + _state, + utilities, + name_mapping=None, + nest_spec=None, + trace_label=None, + trace_choosers=None, + allow_bad_utils=False, + alts_context=None, + alt_nrs_df=None, + ): + captured["alts_context"] = alts_context + captured["alt_nrs_df"] = alt_nrs_df.copy() if alt_nrs_df is not None else None + return pd.Series([0, 0], index=utilities.index), pd.Series( + np.zeros(len(utilities.index)), index=utilities.index + ) + + monkeypatch.setattr( + interaction_sample_simulate.logit, + "make_choices_utility_based", + fake_make_choices_utility_based, + ) + + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_alts_context_forwarding") + + ctx = AltsContext.from_num_alts(3, zero_based=True) + choices = interaction_sample_simulate.interaction_sample_simulate( + state, + choosers, + alternatives, + spec, + choice_column="tdd", + alts_context=ctx, + ) + + assert len(choices) == len(choosers) + assert captured["alts_context"] == ctx + assert captured["alt_nrs_df"] is not None + expected_alt_nrs = pd.DataFrame( + [[0, 2], [0, 2]], + index=choosers.index, + ) + pd.testing.assert_frame_equal(captured["alt_nrs_df"], expected_alt_nrs) + + +def test_interaction_sample_simulate_requires_alts_context_for_eet_integer_choices( + state, +): + state.settings.use_explicit_error_terms = True + + choosers = pd.DataFrame( + {"chooser_attr": [1.0, 1.0]}, + index=pd.Index([200, 201], name="person_id"), + ) + alternatives = pd.DataFrame( + { + "alt_attr": [1.0, 0.5, 0.8, 1.2], + "tdd": [0, 2, 0, 2], + }, + index=pd.Index([200, 200, 201, 201], name="person_id"), + ) + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + with pytest.raises(ValueError, match="alts_context is required"): + interaction_sample_simulate.interaction_sample_simulate( + state, + choosers, + alternatives, + spec, + choice_column="tdd", + ) diff --git a/activitysim/core/test/test_interaction_simulate.py b/activitysim/core/test/test_interaction_simulate.py new file mode 100644 index 0000000000..af9442e228 --- /dev/null +++ b/activitysim/core/test/test_interaction_simulate.py @@ -0,0 +1,174 @@ +# ActivitySim +# See full license in LICENSE.txt. + +import numpy as np +import pandas as pd +import pytest + +from activitysim.core import interaction_simulate, workflow + + +@pytest.fixture +def state() -> workflow.State: + state = workflow.State().default_settings() + state.settings.check_for_variability = False + return state + + +def test_interaction_simulate_explicit_error_terms_parity(state): + # Run interaction_simulate with and without explicit error terms and check that results are similar. + + # Keep this large enough for stable parity checks without overloading CI. + num_choosers = 100_000 + num_alts = 5 + sample_size = num_alts + + # Create random choosers and alternatives + rng = np.random.default_rng(42) + choosers = pd.DataFrame( + {"chooser_attr": rng.random(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + alternatives = pd.DataFrame( + {"alt_attr": rng.random(num_alts)}, + index=pd.Index(range(num_alts), name="alt_id"), + ) + + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["chooser_attr * alt_attr"], name="Expression"), + ) + + # Run _without_ explicit error terms + state.settings.use_explicit_error_terms = False + state.rng().set_base_seed(42) # Set seed BEFORE adding channels or steps + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_mnl") + + choices_mnl = interaction_simulate.interaction_simulate( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + ) + + # Run _with_ explicit error terms + state.init_state() # reset the state to rerun with same seed + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_step_explicit") + + choices_explicit = interaction_simulate.interaction_simulate( + state, + choosers, + alternatives, + spec, + sample_size=sample_size, + ) + + assert len(choices_mnl) == num_choosers + assert len(choices_explicit) == num_choosers + assert choices_mnl.index.equals(choosers.index) + assert choices_explicit.index.equals(choosers.index) + assert not choices_mnl.isna().any() + assert not choices_explicit.isna().any() + + mnl_counts = choices_mnl.value_counts(normalize=True).sort_index() + explicit_counts = choices_explicit.value_counts(normalize=True).sort_index() + + # Check that they are close, relative to the number of draws + assert np.allclose( + mnl_counts.to_numpy(), explicit_counts.to_numpy(), atol=0.01, rtol=0.001 + ) + + +def test_interaction_simulate_eet_unavailable_alternatives(state): + # Test that EET handles unavailable alternatives (very low utilities) + # similarly to MNL (zero probabilities). + + num_choosers = 100 + num_alts = 5 + + choosers = pd.DataFrame( + {"chooser_attr": np.ones(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + # Alt 0 and 1 are attractive, Alt 2, 3, 4 are "unavailable" (very low utility) + alternatives = pd.DataFrame( + {"alt_attr": [10.0, 10.0, -1000.0, -1000.0, -1000.0]}, + index=pd.Index(range(num_alts), name="alt_id"), + ) + + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + # Run with EET + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_unavailable_eet") + + choices_eet = interaction_simulate.interaction_simulate( + state, + choosers, + alternatives, + spec, + sample_size=num_alts, + ) + + assert len(choices_eet) == num_choosers + assert choices_eet.index.equals(choosers.index) + assert not choices_eet.isna().any() + + # Choices should only be from Alt 0 or 1 + assert choices_eet.isin( + [0, 1] + ).all(), f"EET picked an 'unavailable' alternative: {choices_eet[~choices_eet.isin([0, 1])]}" + + +def test_interaction_simulate_eet_large_utilities(state): + # Test that EET handles very large utilities without overflow issues + # that might occur in exp(util) calculations in standard MNL. + + num_choosers = 10 + num_alts = 2 + + choosers = pd.DataFrame( + {"chooser_attr": np.ones(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + # Standard MNL might struggle with exp(700) or exp(800) depending on float precision + alternatives = pd.DataFrame( + {"alt_attr": [700.0, 800.0]}, + index=pd.Index(range(num_alts), name="alt_id"), + ) + + spec = pd.DataFrame( + {"coefficient": [1.0]}, + index=pd.Index(["alt_attr"], name="Expression"), + ) + + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", choosers) + state.rng().begin_step("test_large_utils_eet") + + # This should run without crashing or returning NaNs + choices_eet = interaction_simulate.interaction_simulate( + state, + choosers, + alternatives, + spec, + sample_size=num_alts, + ) + + assert not choices_eet.isna().any() + # With such a large difference, Alt 1 should be the dominant choice + assert (choices_eet == 1).all() diff --git a/activitysim/core/test/test_logit.py b/activitysim/core/test/test_logit.py index 30bae8a3b9..5249e8374a 100644 --- a/activitysim/core/test/test_logit.py +++ b/activitysim/core/test/test_logit.py @@ -10,9 +10,9 @@ import pandas.testing as pdt import pytest - -from activitysim.core import logit, workflow, random -from activitysim.core.logit import add_ev1_random, AltsContext +from activitysim.core import logit, random, simulate, workflow +from activitysim.core.exceptions import InvalidTravelError +from activitysim.core.logit import AltsContext, add_ev1_random from activitysim.core.simulate import eval_variables @@ -73,7 +73,122 @@ def utilities(choosers, spec, test_data): ) -# TODO-EET: Add tests here! +@pytest.fixture(scope="module") +def interaction_choosers(): + return pd.DataFrame({"attr": ["a", "b", "c", "b"]}, index=["w", "x", "y", "z"]) + + +@pytest.fixture(scope="module") +def interaction_alts(): + return pd.DataFrame({"prop": [10, 20, 30, 40]}, index=[1, 2, 3, 4]) + + +# +# Utility Validation Tests +# +def test_validate_utils_replaces_unavailable_values(): + state = workflow.State().default_settings() + utils = pd.DataFrame([[0.0, logit.UTIL_MIN - 1.0], [1.0, 2.0]]) + + validated = logit.validate_utils(state, utils, allow_zero_probs=False) + + assert validated.iloc[0, 0] == pytest.approx(0.0) + assert validated.iloc[0, 1] == pytest.approx(logit.UTIL_UNAVAILABLE) + assert validated.iloc[1, 0] == pytest.approx(1.0) + assert validated.iloc[1, 1] == pytest.approx(2.0) + + +def test_validate_utils_raises_when_all_unavailable(): + state = workflow.State().default_settings() + utils = pd.DataFrame([[logit.UTIL_MIN - 1.0, logit.UTIL_MIN - 2.0]]) + + with pytest.raises(InvalidTravelError) as excinfo: + logit.validate_utils(state, utils, allow_zero_probs=False) + + assert "all probabilities are zero" in str(excinfo.value) + + +def test_validate_utils_allows_zero_probs(): + state = workflow.State().default_settings() + utils = pd.DataFrame([[0.5, logit.UTIL_MIN - 1.0]]) + + validated = logit.validate_utils(state, utils, allow_zero_probs=True) + + assert validated.iloc[0, 0] == 0.5 + assert validated.iloc[0, 1] == logit.UTIL_UNAVAILABLE + + +# +# `utils_to_probs` Tests +# +def test_utils_to_probs_logsums_with_overflow_protection(): + state = workflow.State().default_settings() + utils = pd.DataFrame( + [[1000.0, 1001.0, 999.0], [-1000.0, -1001.0, -999.0]], + columns=["a", "b", "c"], + ) + original_utils = utils.copy() + + probs, logsums = logit.utils_to_probs( + state, + utils, + trace_label=None, + overflow_protection=True, + return_logsums=True, + ) + + utils_np = original_utils.to_numpy() + row_max = utils_np.max(axis=1, keepdims=True) + exp_shifted = np.exp(utils_np - row_max) + expected_probs = exp_shifted / exp_shifted.sum(axis=1, keepdims=True) + expected_logsums = pd.Series( + np.log(exp_shifted.sum(axis=1)) + row_max.squeeze(), + index=utils.index, + ) + + pdt.assert_frame_equal( + probs, + pd.DataFrame(expected_probs, index=utils.index, columns=utils.columns), + rtol=1.0e-7, + atol=0.0, + ) + pdt.assert_series_equal(logsums, expected_logsums, rtol=1.0e-7, atol=0.0) + + +def test_utils_to_probs_warns_on_zero_probs_overflow(): + state = workflow.State().default_settings() + utils = pd.DataFrame( + [[logit.UTIL_MIN - 1.0, logit.UTIL_MIN - 2.0], [0.0, 0.0]], + columns=["a", "b"], + ) + + with pytest.warns(UserWarning, match="cannot set overflow_protection"): + probs = logit.utils_to_probs( + state, + utils, + trace_label=None, + allow_zero_probs=True, + overflow_protection=True, + ) + + assert (probs.iloc[0] == 0.0).all() + assert probs.iloc[1].sum() == pytest.approx(1.0) + assert probs.iloc[1].iloc[0] == pytest.approx(0.5) + assert probs.iloc[1].iloc[1] == pytest.approx(0.5) + + +def test_utils_to_probs_raises_on_float32_zero_probs_overflow(): + state = workflow.State().default_settings() + utils = pd.DataFrame(np.array([[90.0, 0.0]], dtype=np.float32)) + + with pytest.raises(ValueError, match="cannot prevent expected overflow"): + logit.utils_to_probs( + state, + utils, + trace_label=None, + allow_zero_probs=True, + overflow_protection=True, + ) def test_utils_to_probs(utilities, test_data): @@ -122,6 +237,9 @@ def test_utils_to_probs_raises(): assert np.asarray(z).ravel() == pytest.approx(np.asarray([0.0, 0.0, 1.0, 0.0])) +# +# `make_choices` Tests +# def test_make_choices_only_one(): state = workflow.State().default_settings() probs = pd.DataFrame( @@ -205,16 +323,447 @@ def test_make_choices_real_probs(utilities): ) -@pytest.fixture(scope="module") -def interaction_choosers(): - return pd.DataFrame({"attr": ["a", "b", "c", "b"]}, index=["w", "x", "y", "z"]) +def test_different_order_make_choices(): + # check if, when we shuffle utilities, make_choices chooses the same alternatives + state = workflow.State().default_settings() + # increase number of choosers and alternatives for realism + n_choosers = 100 + n_alts = 50 + data = np.random.rand(n_choosers, n_alts) + chooser_ids = np.arange(n_choosers) + alt_ids = [f"alt_{i}" for i in range(n_alts)] + + utilities = pd.DataFrame( + data, + index=pd.Index(chooser_ids, name="chooser_id"), + columns=alt_ids, + ) -@pytest.fixture(scope="module") -def interaction_alts(): - return pd.DataFrame({"prop": [10, 20, 30, 40]}, index=[1, 2, 3, 4]) + # We need a stable RNG that gives the same random numbers for the same chooser_id + # regardless of row order. ActivitySim's random.Random does this. + state.get_rn_generator().add_channel("chooser_id", utilities) + state.get_rn_generator().begin_step("test_step") + + probs = logit.utils_to_probs(state, utilities, trace_label=None) + choices, rands = logit.make_choices(state, probs) + + # shuffle utilities (rows) and make_choices again + # We must reset the step offset so the RNG produces the same sequence for the same IDs + state.get_rn_generator().end_step("test_step") + state.get_rn_generator().begin_step("test_step") + utilities_shuffled = utilities.sample(frac=1, random_state=42) + probs_shuffled = logit.utils_to_probs(state, utilities_shuffled, trace_label=None) + choices_shuffled, rands_shuffled = logit.make_choices(state, probs_shuffled) + + # sorting both to ensure comparison is on the same index order + pdt.assert_series_equal( + choices.sort_index(), choices_shuffled.sort_index(), check_dtype=False + ) + + +def test_make_choices_matches_random_draws(): + class DummyRNG: + def random_for_df(self, df, n=1): + assert n == 1 + return np.array([[0.05], [0.6], [0.95]]) + + class DummyState: + @staticmethod + def get_rn_generator(): + return DummyRNG() + + state = DummyState() + probs = pd.DataFrame( + [[0.1, 0.2, 0.7], [0.4, 0.4, 0.2], [0.05, 0.9, 0.05]], + index=["a", "b", "c"], + columns=["x", "y", "z"], + ) + choices, rands = logit.make_choices(state, probs) + + expected_rands = np.array([0.05, 0.6, 0.95]) + expected_choices = np.array([0, 1, 1]) + + pdt.assert_series_equal( + rands, + pd.Series(expected_rands, index=probs.index), + check_names=False, + ) + pdt.assert_series_equal( + choices, + pd.Series(expected_choices, index=probs.index), + check_dtype=False, + ) + + +# +# EV1 Random Tests +# +def test_add_ev1_random(): + class DummyRNG: + def gumbel_for_df(self, df, n): + # Deterministic, non-constant draws make it easy to verify + # correct per-row/per-column addition behavior. + row_component = df.index.to_numpy(dtype=float).reshape(-1, 1) / 10.0 + col_component = np.arange(n, dtype=float).reshape(1, -1) + return row_component + col_component + + rng = DummyRNG() + + class DummyState: + @staticmethod + def get_rn_generator(): + return rng + + utilities = pd.DataFrame( + [[1.0, 2.0], [3.0, 4.0]], + index=[10, 11], + columns=["a", "b"], + ) + + randomized = logit.add_ev1_random(DummyState(), utilities) + + expected = pd.DataFrame( + [[2.0, 4.0], [4.1, 6.1]], + index=[10, 11], + columns=["a", "b"], + ) + + # check that the random component was added correctly, and that the original utilities were not mutated + pdt.assert_frame_equal(randomized, expected) + pdt.assert_index_equal(randomized.index, utilities.index) + pdt.assert_index_equal(randomized.columns, utilities.columns) + pdt.assert_frame_equal( + utilities, + pd.DataFrame( + [[1.0, 2.0], [3.0, 4.0]], + index=[10, 11], + columns=["a", "b"], + ), + ) + + +def test_add_ev1_random_requires_paired_alt_context_args(): + class DummyRNG: + def gumbel_for_df(self, df, n): + return np.zeros((len(df), n)) + + class DummyState: + @staticmethod + def get_rn_generator(): + return DummyRNG() + + utilities = pd.DataFrame([[1.0, 2.0]], index=[1], columns=["a", "b"]) + + with pytest.raises( + AssertionError, + match="alt_info and alt_nrs_df must both be provided or omitted together", + ): + logit.add_ev1_random( + DummyState(), + utilities, + alt_info=AltsContext.from_num_alts(2), + alt_nrs_df=None, + ) + + +# +# Nested Logit Structure Tests +# +def test_group_nest_names_by_level(): + nest_spec = { + "name": "root", + "coefficient": 1.0, + "alternatives": [ + {"name": "motorized", "coefficient": 0.7, "alternatives": ["car", "bus"]}, + "walk", + ], + } + + grouped = logit.group_nest_names_by_level(nest_spec) + + assert grouped == {1: ["root"], 2: ["motorized", "walk"], 3: ["car", "bus"]} + + +def test_choose_from_tree_selects_leaf(): + nest_utils = pd.Series( + { + "motorized": 2.0, + "walk": 1.0, + "car": 5.0, + "bus": 3.0, + } + ) + all_alternatives = {"walk", "car", "bus"} + logit_nest_groups = {1: ["root"], 2: ["motorized", "walk"], 3: ["car", "bus"]} + nest_alternatives_by_name = { + "root": ["motorized", "walk"], + "motorized": ["car", "bus"], + } + + choice = logit.choose_from_tree( + nest_utils, all_alternatives, logit_nest_groups, nest_alternatives_by_name + ) + + assert choice == "car" + + +def test_choose_from_tree_raises_on_missing_leaf(): + nest_utils = pd.Series({"motorized": 2.0, "walk": 1.0}) + all_alternatives = {"car", "bus"} + logit_nest_groups = {1: ["root"], 2: ["motorized", "walk"]} + nest_alternatives_by_name = { + "root": ["motorized", "walk"], + "motorized": ["car", "bus"], + } + + with pytest.raises(ValueError, match="no alternative found"): + logit.choose_from_tree( + nest_utils, all_alternatives, logit_nest_groups, nest_alternatives_by_name + ) + + +# +# EET Choice Behavior Tests +# +def test_make_choices_eet_mnl(monkeypatch): + def fake_add_ev1_random(_state, _df, alt_info=None, alt_nrs_df=None): + return pd.DataFrame( + [[1.0, 3.0], [4.0, 2.0]], + index=[100, 101], + columns=["a", "b"], + ) + + monkeypatch.setattr(logit, "add_ev1_random", fake_add_ev1_random) + + choices = logit.make_choices_explicit_error_term_mnl( + workflow.State().default_settings(), + pd.DataFrame([[0.0, 0.0], [0.0, 0.0]], index=[100, 101], columns=["a", "b"]), + trace_label=None, + ) + + pdt.assert_series_equal(choices, pd.Series([1, 0], index=[100, 101])) + + +def test_make_choices_eet_nl(monkeypatch): + def fake_add_ev1_random(_state, _df, alt_info=None, alt_nrs_df=None): + return pd.DataFrame( + [[5.0, 1.0, 4.0, 2.0], [3.0, 4.0, 1.0, 2.0]], + index=[10, 11], + columns=["motorized", "walk", "car", "bus"], + ) + + monkeypatch.setattr(logit, "add_ev1_random", fake_add_ev1_random) + + nest_spec = { + "name": "root", + "coefficient": 1.0, + "alternatives": [ + {"name": "motorized", "coefficient": 0.7, "alternatives": ["car", "bus"]}, + "walk", + ], + } + alt_order_array = np.array(["walk", "car", "bus"]) + + choices = logit.make_choices_explicit_error_term_nl( + workflow.State().default_settings(), + pd.DataFrame( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]], + index=[10, 11], + columns=["motorized", "walk", "car", "bus"], + ), + alt_order_array, + nest_spec, + trace_label=None, + ) + + pdt.assert_series_equal(choices, pd.Series([1, 0], index=[10, 11])) + + +def test_make_choices_utility_based_sets_zero_rands(monkeypatch): + def fake_add_ev1_random(_state, df, alt_info=None, alt_nrs_df=None): + return pd.DataFrame( + [[2.0, 1.0], [0.5, 2.5]], + index=df.index, + columns=df.columns, + ) + + monkeypatch.setattr(logit, "add_ev1_random", fake_add_ev1_random) + + utilities = pd.DataFrame([[3.0, 2.0], [1.0, 4.0]], index=[11, 12]) + choices, rands = logit.make_choices_utility_based( + workflow.State().default_settings(), + utilities, + name_mapping=np.array(["a", "b"]), + nest_spec=None, + trace_label=None, + ) + + expected_choices = pd.Series([0, 1], index=[11, 12]) + pdt.assert_series_equal(choices, expected_choices) + pdt.assert_series_equal(rands, pd.Series([0, 0], index=[11, 12])) + + +# +# EET vs non-EET Choice Behavior Tests +# +def test_make_choices_vs_eet_same_distribution(): + """With many draws, make_choices (probability-based) and + make_choices_explicit_error_term_mnl (EET) should produce roughly the + same empirical choice-frequency distribution for the same utilities.""" + n_draws = 1_000_000 + a_tol = 0.001 + r_tol = 0.01 + utils_values = [5.0, 6.0, 7.0, 8.0, 9.0] + n_alts = len(utils_values) + columns = ["a", "b", "c", "d", "e"] + + utils = pd.DataFrame([utils_values] * n_draws, columns=columns) + + # Probability-based (Monte Carlo) path — independent RNG + mc_rng = np.random.default_rng(42) + + class MCDummyRNG: + def random_for_df(self, df, n=1): + return mc_rng.random((len(df), n)) + + class MCDummyState: + @staticmethod + def get_rn_generator(): + return MCDummyRNG() + + probs = logit.utils_to_probs( + MCDummyState(), utils, trace_label=None, overflow_protection=True + ) + choices_mc, _ = logit.make_choices(MCDummyState(), probs, trace_label=None) + + # Explicit-error-term (EET) path — independent RNG + eet_rng = np.random.default_rng(123) + + class EETDummyRNG: + def gumbel_for_df(self, df, n): + return eet_rng.gumbel(size=(len(df), n)) + + class EETDummyState: + @staticmethod + def get_rn_generator(): + return EETDummyRNG() + + choices_eet = logit.make_choices_explicit_error_term_mnl( + EETDummyState(), utils, trace_label=None + ) + + mc_fracs = np.bincount(choices_mc.values.astype(int), minlength=n_alts) / n_draws + eet_fracs = np.bincount(choices_eet.values.astype(int), minlength=n_alts) / n_draws + + np.testing.assert_allclose(mc_fracs, eet_fracs, atol=a_tol, rtol=r_tol) + np.testing.assert_allclose( + mc_fracs, probs.iloc[0].to_numpy(), atol=a_tol, rtol=r_tol + ) + np.testing.assert_allclose( + eet_fracs, probs.iloc[0].to_numpy(), atol=a_tol, rtol=r_tol + ) + + +def test_make_choices_vs_eet_nl_same_distribution(): + """With many draws, nested logit choices via probabilities and + nested logit choices via EET should produce the same empirical distribution.""" + n_draws = 100_000 + a_tol = 0.01 + + nest_spec = { + "name": "root", + "coefficient": 1.0, + "alternatives": [ + {"name": "motorized", "coefficient": 0.5, "alternatives": ["car", "bus"]}, + "walk", + ], + } + # Utilities for car, bus, walk + # For NL, we need utilities for all nodes in the tree for EET, + # but for probability-based choice we usually use the flattened/logsummed probabilities. + # To compare them fairly, we use the same base utilities. + # car=0.5, bus=0.2, walk=0.4 + utils_df = pd.DataFrame( + [[0.5, 0.2, 0.4, 0.0, 0.0]], + columns=["car", "bus", "walk", "motorized", "root"], + ) + utils_df = pd.concat([utils_df] * n_draws, ignore_index=True) + alt_order_array = np.array(["car", "bus", "walk"]) + + # 1. Probability-based Nested Logit choices + mc_rng = np.random.default_rng(42) + + class MCDummyRNG: + def random_for_df(self, df, n=1): + return mc_rng.random((len(df), n)) + + class MCDummyState: + @staticmethod + def get_rn_generator(): + return MCDummyRNG() + + def default_settings(self): + return self + + # Compute probabilities for NL using simulation logic + nested_exp_utilities = simulate.compute_nested_exp_utilities( + utils_df[["car", "bus", "walk"]], nest_spec + ) + nested_probabilities = simulate.compute_nested_probabilities( + MCDummyState(), nested_exp_utilities, nest_spec, trace_label=None + ) + probs = simulate.compute_base_probabilities( + nested_probabilities, nest_spec, utils_df[["car", "bus", "walk"]] + ) + choices_mc, _ = logit.make_choices(MCDummyState(), probs, trace_label=None) + + # 2. EET-based Nested Logit choices + eet_rng = np.random.default_rng(123) + + class EETDummyRNG: + def gumbel_for_df(self, df, n): + return eet_rng.gumbel(size=(len(df), n)) + + class EETDummyState: + @staticmethod + def get_rn_generator(): + return EETDummyRNG() + + def default_settings(self): + return self + + @property + def tracing(self): + import activitysim.core.tracing as tracing + + return tracing + + # For EET NL, we provide the utilities for all nodes. + # compute_nested_utilities handles the division by nesting coefficients for leaves + # and the logsum * coefficient for internal nodes. + nested_utilities = simulate.compute_nested_utilities( + utils_df[["car", "bus", "walk"]], nest_spec + ) + + choices_eet = logit.make_choices_explicit_error_term_nl( + EETDummyState(), + nested_utilities, + alt_order_array, + nest_spec, + trace_label=None, + ) + mc_fracs = np.bincount(choices_mc.values.astype(int), minlength=3) / n_draws + eet_fracs = np.bincount(choices_eet.values.astype(int), minlength=3) / n_draws + # They should be close + np.testing.assert_allclose(mc_fracs, eet_fracs, atol=a_tol) + + +# +# Interaction Dataset Tests +# def test_interaction_dataset_no_sample(interaction_choosers, interaction_alts): expected = pd.DataFrame( { @@ -229,9 +778,6 @@ def test_interaction_dataset_no_sample(interaction_choosers, interaction_alts): ) interacted, expected = interacted.align(expected, axis=1) - - print("interacted\n", interacted) - print("expected\n", expected) pdt.assert_frame_equal(interacted, expected) @@ -253,3 +799,99 @@ def test_interaction_dataset_sampled(interaction_choosers, interaction_alts): interacted, expected = interacted.align(expected, axis=1) pdt.assert_frame_equal(interacted, expected) + + +def reset_step(state, name="test_step"): + state.get_rn_generator().end_step(name) + state.get_rn_generator().begin_step(name) + + +def test_make_choices_utility_based_sampled_alts(): + """Test the situation of making choices from a sampled choice set""" + # TODO should these tests go in test_random? + state = workflow.State().default_settings() + # Make explicit that there's two indexing schemes - the raw alts, and the 0 based internals + utils_project_raw = pd.DataFrame( + {"a": 10.582999, "b": 10.680792, "c": 10.710443}, + index=pd.Index([0], name="person_id"), + ) + # zero based indexes + utils_project = utils_project_raw.rename(columns={"a": 0, "b": 1, "c": 2}) + utils_base = utils_project_raw[["a", "c"]].rename(columns={"a": 0, "c": 1}) + + assert utils_project.index.name == "person_id" + state.get_rn_generator().add_channel("persons", utils_project) + state.get_rn_generator().begin_step("test_step") + # mock base case, where alt 1 is omitted (it was improved in the project) + # this situation is quite common with poisson sampling with a variable choice set size, + # but it can also happen in with-replacement EET sampling e.g. if alt 2 had a pick_count of 2 in the base case. + # In principle, it can also be problematic for non-sampled choices where there is a base project difference in the + # availability of alternatives .e.g a new mode was introduced in the project case + + utils_project_with_rands = add_ev1_random(state, utils_project) + rands_project = utils_project_with_rands - utils_project + reset_step(state) + utils_base_with_rands = add_ev1_random(state, utils_base) + rands_base = utils_base_with_rands - utils_base + rands_base_labeled = rands_base.rename(columns={0: "a", 1: "c"}) + rands_project_labeled = rands_project.rename(columns={0: "a", 1: "b", 2: "c"}) + with pytest.raises( + AssertionError, match=re.escape('(column name="c") are different') + ): + # TODO this should pass + pdt.assert_frame_equal( + rands_base_labeled, rands_project_labeled.loc[:, rands_base_labeled.columns] + ) + # document incorrect invariant - first two columns have the same random numbers: + pdt.assert_frame_equal(rands_base, rands_project.iloc[:, :2]) + + # revised approach + reset_step(state) + alt_nrs_df = pd.DataFrame({0: 0, 1: 1, 2: 2}, index=utils_project_raw.index) + alt_info = AltsContext.from_num_alts(3, zero_based=True) + utils_project_with_rands = add_ev1_random( + state, utils_project, alt_info=alt_info, alt_nrs_df=alt_nrs_df + ) + rands_project = utils_project_with_rands - utils_project + reset_step(state) + + # alt "b" is missing from the sampled choice set, alt_nrs_df is set to reflect that + alt_nrs_df = pd.DataFrame({0: 0, 1: 2}, index=utils_project_raw.index) + utils_base_with_rands = add_ev1_random( + state, utils_base, alt_info=alt_info, alt_nrs_df=alt_nrs_df + ) + rands_base = utils_base_with_rands - utils_base + rands_base_labeled = rands_base.rename(columns={0: "a", 1: "c"}) + rands_project_labeled = rands_project.rename(columns={0: "a", 1: "b", 2: "c"}) + + # Corrected invariant holds true + pdt.assert_frame_equal( + rands_base_labeled, rands_project_labeled.loc[:, rands_base_labeled.columns] + ) + + +def test_alts_context_from_series_and_properties(): + ctx = AltsContext.from_series(pd.Index([3, 5, 9, 4])) + + assert ctx.min_alt_id == 3 + assert ctx.max_alt_id == 9 + assert ctx.n_alts_to_cover_max_id == 10 + assert ctx.n_rands_to_sample == 10 + + +@pytest.mark.parametrize( + "num_alts,zero_based,expected_min,expected_max,expected_n_cover", + [ + (5, True, 0, 4, 5), + (5, False, 1, 5, 6), + ], +) +def test_alts_context_from_num_alts( + num_alts, zero_based, expected_min, expected_max, expected_n_cover +): + ctx = AltsContext.from_num_alts(num_alts=num_alts, zero_based=zero_based) + + assert ctx.min_alt_id == expected_min + assert ctx.max_alt_id == expected_max + assert ctx.n_alts_to_cover_max_id == expected_n_cover + assert ctx.n_rands_to_sample == expected_n_cover diff --git a/activitysim/core/test/test_simulate.py b/activitysim/core/test/test_simulate.py index 17d4ba2cd6..21e0f90e73 100644 --- a/activitysim/core/test/test_simulate.py +++ b/activitysim/core/test/test_simulate.py @@ -10,7 +10,7 @@ import pandas.testing as pdt import pytest -from activitysim.core import simulate, workflow +from activitysim.core import chunk, simulate, workflow @pytest.fixture @@ -42,6 +42,19 @@ def data(data_dir): return pd.read_csv(os.path.join(data_dir, "data.csv")) +@pytest.fixture +def nest_spec(): + nest_spec = { + "name": "root", + "coefficient": 1.0, + "alternatives": [ + {"name": "alt0", "coefficient": 0.5, "alternatives": ["alt0.0", "alt0.1"]}, + "alt1", + ], + } + return nest_spec + + def test_read_model_spec(state, spec_name): spec = state.filesystem.read_model_spec(file_name=spec_name) @@ -88,3 +101,234 @@ def test_simple_simulate_chunked(state, data, spec): ) expected = pd.Series([1, 1, 1], index=data.index) pdt.assert_series_equal(choices, expected, check_dtype=False) + + +def test_eval_mnl_eet(state): + # Check that the same counts are returned by eval_mnl when using EET and when not. + + num_choosers = 100_000 + + np.random.seed(42) + data2 = pd.DataFrame( + { + "chooser_attr": np.random.rand(num_choosers), + }, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + spec2 = pd.DataFrame( + {"alt0": [1.0], "alt1": [2.0]}, + index=pd.Index(["chooser_attr"], name="Expression"), + ) + + # Set up a state with EET enabled + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", data2) + state.rng().begin_step("test_step_mnl") + + chunk_sizer = chunk.ChunkSizer(state, "", "", num_choosers) + + # run eval_mnl with EET enabled + choices_eet = simulate.eval_mnl( + state=state, + choosers=data2, + spec=spec2, + locals_d=None, + custom_chooser=None, + estimator=None, + chunk_sizer=chunk_sizer, + ) + + # Reset the state, without EET enabled + state.settings.use_explicit_error_terms = False + + state.rng().end_step("test_step_mnl") + state.rng().begin_step("test_step_mnl") + + choices_mnl = simulate.eval_mnl( + state=state, + choosers=data2, + spec=spec2, + locals_d=None, + custom_chooser=None, + estimator=None, + chunk_sizer=chunk_sizer, + ) + + # Compare counts + mnl_counts = choices_mnl.value_counts(normalize=True) + explicit_counts = choices_eet.value_counts(normalize=True) + assert np.allclose(mnl_counts, explicit_counts, atol=0.01) + + +def test_eval_nl_eet(state, nest_spec): + # Check that the same counts are returned by eval_nl when using EET and when not. + + num_choosers = 100_000 + + np.random.seed(42) + data2 = pd.DataFrame( + { + "chooser_attr": np.random.rand(num_choosers), + }, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + spec2 = pd.DataFrame( + {"alt1": [2.0], "alt0.0": [0.5], "alt0.1": [0.2]}, + index=pd.Index(["chooser_attr"], name="Expression"), + ) + + # Set up a state with EET enabled + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", data2) + state.rng().begin_step("test_step_mnl") + + chunk_sizer = chunk.ChunkSizer(state, "", "", num_choosers) + + # run eval_nl with EET enabled + choices_eet = simulate.eval_nl( + state=state, + choosers=data2, + spec=spec2, + nest_spec=nest_spec, + locals_d={}, + custom_chooser=None, + estimator=None, + trace_label="test", + chunk_sizer=chunk_sizer, + ) + + # Reset the state, without EET enabled + state.settings.use_explicit_error_terms = False + + state.rng().end_step("test_step_mnl") + state.rng().begin_step("test_step_mnl") + + choices_mnl = simulate.eval_nl( + state=state, + choosers=data2, + spec=spec2, + nest_spec=nest_spec, + locals_d={}, + custom_chooser=None, + trace_label="test", + estimator=None, + chunk_sizer=chunk_sizer, + ) + + # Compare counts + mnl_counts = choices_mnl.value_counts(normalize=True) + explicit_counts = choices_eet.value_counts(normalize=True) + assert np.allclose(mnl_counts, explicit_counts, atol=0.01) + + +def test_compute_nested_utilities(nest_spec): + # computes nested utilities manually and using the function and checks that + # the utilities are the same + + num_choosers = 2 + raw_utilities = pd.DataFrame( + {"alt1": [1, 10], "alt0.0": [2, 3], "alt0.1": [4, 5]}, + index=pd.Index(range(num_choosers)), + ) + + nested_utilities = simulate.compute_nested_utilities(raw_utilities, nest_spec) + + # these are from the definition of nest_spec + alt0_nest_coefficient = nest_spec["alternatives"][0]["coefficient"] + alt0_leaf_product_of_coefficients = nest_spec["coefficient"] * alt0_nest_coefficient + assert alt0_leaf_product_of_coefficients == 0.5 # 1 * 0.5 + + product_of_coefficientss = pd.DataFrame( + { + "alt1": [nest_spec["coefficient"]], + "alt0.0": [alt0_leaf_product_of_coefficients], + "alt0.1": [alt0_leaf_product_of_coefficients], + }, + index=[0], + ) + leaf_utilities = raw_utilities / product_of_coefficientss.iloc[0] + + constructed_nested_utilities = pd.DataFrame(index=raw_utilities.index) + + constructed_nested_utilities[leaf_utilities.columns] = leaf_utilities + constructed_nested_utilities["alt0"] = alt0_nest_coefficient * np.log( + np.exp(leaf_utilities[["alt0.0", "alt0.1"]]).sum(axis=1) + ) + constructed_nested_utilities["root"] = nest_spec["coefficient"] * np.log( + np.exp(constructed_nested_utilities[["alt1", "alt0"]]).sum(axis=1) + ) + + assert np.allclose( + nested_utilities, constructed_nested_utilities[nested_utilities.columns] + ), "Mismatch in nested utilities" + + +def test_eval_nl_logsums_eet_vs_non_eet(state, nest_spec): + """eval_nl with want_logsums=True must produce identical logsums under + EET and non-EET modes""" + + num_choosers = 100 + + np.random.seed(42) + data2 = pd.DataFrame( + {"chooser_attr": np.random.rand(num_choosers)}, + index=pd.Index(range(num_choosers), name="person_id"), + ) + + spec2 = pd.DataFrame( + {"alt1": [2.0], "alt0.0": [0.5], "alt0.1": [0.2]}, + index=pd.Index(["chooser_attr"], name="Expression"), + ) + + chunk_sizer = chunk.ChunkSizer(state, "", "", num_choosers) + + state.settings.use_explicit_error_terms = True + state.rng().set_base_seed(42) + state.rng().add_channel("person_id", data2) + state.rng().begin_step("test_step_logsums") + + result_eet = simulate.eval_nl( + state=state, + choosers=data2, + spec=spec2, + nest_spec=nest_spec, + locals_d={}, + custom_chooser=None, + estimator=None, + want_logsums=True, + trace_label="test", + chunk_sizer=chunk_sizer, + ) + + state.rng().end_step("test_step_logsums") + + state.settings.use_explicit_error_terms = False + state.rng().begin_step("test_step_logsums") + + result_non_eet = simulate.eval_nl( + state=state, + choosers=data2, + spec=spec2, + nest_spec=nest_spec, + locals_d={}, + custom_chooser=None, + estimator=None, + want_logsums=True, + trace_label="test", + chunk_sizer=chunk_sizer, + ) + + state.rng().end_step("test_step_logsums") + + # Both paths should return a DataFrame with 'choice' and 'logsum' columns + assert "logsum" in result_eet.columns, "EET result missing logsum column" + assert "logsum" in result_non_eet.columns, "non-EET result missing logsum column" + + # Logsums are deterministic — they must be identical across paths + assert np.allclose( + result_eet["logsum"].values, result_non_eet["logsum"].values, rtol=1e-10 + ) diff --git a/activitysim/examples/placeholder_multiple_zone/test/configs_eet/settings.yaml b/activitysim/examples/placeholder_multiple_zone/test/configs_eet/settings.yaml new file mode 100644 index 0000000000..08c06d702e --- /dev/null +++ b/activitysim/examples/placeholder_multiple_zone/test/configs_eet/settings.yaml @@ -0,0 +1,3 @@ +inherit_settings: True + +use_explicit_error_terms: True diff --git a/activitysim/examples/placeholder_multiple_zone/test/reference_pipeline_2_zone_eet.zip b/activitysim/examples/placeholder_multiple_zone/test/reference_pipeline_2_zone_eet.zip new file mode 100644 index 0000000000000000000000000000000000000000..8e3abdfff6beb2d8c8e9fa0d20aad9280e7345ab GIT binary patch literal 285194 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

SgtnX+F#R*VryE*6 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 literal 0 HcmV?d00001 diff --git a/activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_tours_2_zone.csv b/activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_tours_2_zone.csv new file mode 100644 index 0000000000..00aa4c0fc7 --- /dev/null +++ b/activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_tours_2_zone.csv @@ -0,0 +1,106 @@ +"person_id","tour_type","tour_type_count","tour_type_num","tour_num","tour_count","tour_category","number_of_participants","destination","origin","household_id","tdd","start","end","duration","composition","destination_logsum","tour_mode","mode_choice_logsum","atwork_subtour_frequency","parent_tour_id","stop_frequency","primary_purpose","tour_id" +26686,"shopping",1,1,1,1,"non_mandatory",1,23000,8000,26686,113,12,13,1,"",13.652449170814883,"WALK_LRF",1.0501470508061868,"",,"0out_0in","shopping",1094159 +26844,"othmaint",1,1,2,2,"non_mandatory",1,5000,8000,26844,159,16,21,5,"",15.525049977674522,"WALK_LOC",2.760890821002089,"",,"0out_0in","othmaint",1100632 +26844,"shopping",1,1,1,2,"non_mandatory",1,1000,8000,26844,75,9,14,5,"",14.275587915746392,"TNC_SINGLE",2.305056815108128,"",,"1out_0in","shopping",1100637 +27726,"eatout",1,1,1,1,"non_mandatory",1,10000,10000,27726,140,14,19,5,"",15.203007240171102,"WALK",4.342645736471603,"",,"0out_0in","eatout",1136772 +110675,"work",1,1,1,1,"mandatory",1,16000,16000,110675,13,5,18,13,"",,"WALK",-0.2730591306116382,"no_subtours",,"0out_0in","work",4537714 +112064,"work",1,1,1,1,"mandatory",1,24000,16000,112064,131,13,20,7,"",,"WALK_LOC",5.176494664984331,"no_subtours",,"1out_1in","work",4594663 +264108,"eatout",1,1,1,1,"non_mandatory",1,8000,9000,226869,135,14,14,0,"",13.203967734929993,"WALK",1.0687903457949945,"",,"0out_0in","eatout",10828434 +323689,"work",1,1,1,1,"mandatory",1,2000,10000,256660,151,15,21,6,"",,"WALK_LRF",5.943591391097562,"no_subtours",,"1out_0in","work",13271288 +323690,"work",1,1,1,1,"mandatory",1,9000,10000,256660,117,12,17,5,"",,"WALK",5.3957995007835535,"no_subtours",,"0out_1in","work",13271329 +325431,"othdiscr",1,1,1,1,"non_mandatory",1,22000,16000,257531,102,11,14,3,"",15.268405514775877,"WALK",2.165017948111244,"",,"0out_2in","othdiscr",13342696 +325431,"work",1,1,1,1,"mandatory",1,14000,16000,257531,157,16,19,3,"",,"WALK_LOC",5.979950151025447,"no_subtours",,"0out_0in","work",13342710 +325432,"work",1,1,1,1,"mandatory",1,15000,16000,257531,45,7,15,8,"",,"WALK_LOC",5.9577256558570015,"no_subtours",,"0out_0in","work",13342751 +595684,"escort",1,1,1,1,"non_mandatory",1,5000,21000,370497,38,7,8,1,"",12.420811407080112,"SHARED3FREE",-1.0951775798823786,"",,"0out_0in","escort",24423053 +595684,"work",1,1,1,1,"mandatory",1,19000,21000,370497,167,17,22,5,"",,"SHARED2FREE",-0.14535197835485364,"no_subtours",,"3out_0in","work",24423083 +595685,"school",1,1,1,1,"mandatory",1,13000,21000,370497,61,8,15,7,"",,"WALK_LOC",-0.9348277771487143,"",,"0out_0in","school",24423116 +595686,"school",1,1,1,1,"mandatory",1,21000,21000,370497,41,7,11,4,"",,"WALK",-0.41324468118526936,"",,"0out_0in","school",24423157 +644292,"school",1,1,1,1,"mandatory",1,2000,7000,386699,9,5,14,9,"",,"WALK",18.86976579102885,"",,"0out_0in","school",26416003 +644476,"work",1,1,1,1,"mandatory",1,7000,16000,386761,47,7,17,10,"",,"WALK_LOC",5.51666670545706,"no_subtours",,"0out_0in","work",26423555 +644477,"work",1,1,1,1,"mandatory",1,4000,16000,386761,64,8,18,10,"",,"WALK_LOC",5.662108100914558,"no_subtours",,"0out_2in","work",26423596 +644478,"school",1,1,1,1,"mandatory",1,25000,16000,386761,45,7,15,8,"",,"WALK_LOC",17.88652386604347,"",,"0out_0in","school",26423629 +1267567,"eatout",1,1,1,1,"non_mandatory",1,11000,21000,570454,99,11,11,0,"",15.277431294707508,"WALK",4.004645419133042,"",,"0out_0in","eatout",51970253 +1427193,"shopping",1,1,1,1,"non_mandatory",1,16000,25000,703381,151,15,21,6,"",13.171561237606278,"BIKE",0.8981575547418779,"",,"0out_0in","shopping",58514946 +1427194,"othmaint",3,1,1,3,"non_mandatory",1,9000,25000,703381,74,9,13,4,"",14.416957607852858,"BIKE",0.6613874806788961,"",,"0out_0in","othmaint",58514982 +1427194,"othmaint",3,2,2,3,"non_mandatory",1,8000,25000,703381,156,16,18,2,"",14.365283875141941,"BIKE",0.6990093306817798,"",,"0out_0in","othmaint",58514983 +1427194,"othmaint",3,3,3,3,"non_mandatory",1,7000,25000,703381,172,18,21,3,"",14.373474385937751,"BIKE",1.1745535385516204,"",,"0out_0in","othmaint",58514984 +1572659,"othdiscr",1,1,1,1,"non_mandatory",1,7000,6000,763879,8,5,13,8,"",15.269403956266437,"WALK",3.1705302835155984,"",,"0out_0in","othdiscr",64479044 +1572930,"eatout",1,1,1,1,"non_mandatory",1,12000,9000,764150,46,7,16,9,"",15.42100489856711,"WALK",3.628422928001604,"",,"0out_0in","eatout",64490136 +1632206,"work",1,1,1,1,"mandatory",1,12000,11000,823426,48,7,18,11,"",,"WALK",-0.0229685389951619,"no_subtours",,"0out_0in","work",66920485 +1632281,"work",1,1,1,1,"mandatory",1,1000,12000,823501,64,8,18,10,"",,"WALK",-0.4832251215332097,"no_subtours",,"0out_0in","work",66923560 +1632987,"eat",1,1,1,1,"atwork",1,13000,22000,824207,100,11,12,1,"",15.37146933803088,"TNC_SINGLE",5.527382364048134,"",66952506,"0out_0in","atwork",66952471 +1632987,"work",1,1,1,1,"mandatory",1,22000,18000,824207,50,7,20,13,"",,"WALK_LRF",5.930592339624059,"eat",,"0out_0in","work",66952506 +1875721,"work",1,1,1,1,"mandatory",1,4000,16000,982875,49,7,19,12,"",,"SHARED3FREE",1.731505679041429,"no_subtours",,"0out_0in","work",76904600 +1875722,"work",1,1,1,1,"mandatory",1,7000,16000,982875,48,7,18,11,"",,"WALK",1.5259948396881422,"no_subtours",,"0out_0in","work",76904641 +2159057,"work",1,1,1,1,"mandatory",1,11000,20000,1099626,47,7,17,10,"",,"BIKE",0.08239658913473709,"no_subtours",,"0out_0in","work",88521376 +2159058,"school",1,1,1,1,"mandatory",1,12000,20000,1099626,44,7,14,7,"",,"WALK_LOC",-0.08092637357224697,"",,"0out_0in","univ",88521409 +2159059,"school",1,1,1,1,"mandatory",1,17000,20000,1099626,61,8,15,7,"",,"SHARED2FREE",-0.5795062930092348,"",,"0out_0in","school",88521450 +2458500,"othdiscr",1,1,1,1,"non_mandatory",1,2000,8000,1173905,126,13,15,2,"",15.003025752404032,"TNC_SINGLE",2.1345972053076645,"",,"0out_0in","othdiscr",100798525 +2458502,"school",1,1,1,1,"mandatory",1,9000,8000,1173905,76,9,15,6,"",,"WALK_LOC",19.061076416939844,"",,"0out_0in","school",100798613 +2458503,"school",1,1,1,1,"mandatory",1,25000,8000,1173905,63,8,17,9,"",,"WALK",17.394445009270893,"",,"0out_0in","school",100798654 +2566698,"othmaint",1,1,1,1,"non_mandatory",1,12000,25000,1196298,146,15,16,1,"",13.904676146693486,"WALK",-0.08705033093047729,"",,"0out_0in","othmaint",105234646 +2566698,"work",1,1,1,1,"mandatory",1,9000,25000,1196298,42,7,12,5,"",,"SHARED3FREE",-0.12492841393683855,"no_subtours",,"1out_2in","work",105234657 +2566699,"escort",2,1,1,4,"non_mandatory",1,9000,25000,1196298,55,8,9,1,"",12.487156714808382,"SHARED3FREE",-1.224321337819968,"",,"0out_0in","escort",105234668 +2566699,"escort",2,2,2,4,"non_mandatory",1,2000,25000,1196298,112,12,12,0,"",12.473008939270755,"SHARED2FREE",-0.5133849848564047,"",,"0out_0in","escort",105234669 +2566699,"othdiscr",1,1,4,4,"non_mandatory",1,5000,25000,1196298,172,18,21,3,"",13.96308058011918,"WALK",0.6917454521182999,"",,"0out_0in","othdiscr",105234684 +2566699,"shopping",1,1,3,4,"non_mandatory",1,17000,25000,1196298,71,9,10,1,"",12.746190439180568,"TNC_SINGLE",-0.323622964459322,"",,"0out_0in","shopping",105234692 +2566700,"school",1,1,1,1,"mandatory",1,19000,25000,1196298,61,8,15,7,"",,"BIKE",-1.7110584511799807,"",,"0out_0in","school",105234731 +2566701,"escort",1,1,1,1,"non_mandatory",1,13000,25000,1196298,124,13,13,0,"",12.587432709712925,"SHARED2FREE",0.06051195773469012,"",,"0out_0in","escort",105234750 +2566701,"school",1,1,1,1,"mandatory",1,11000,25000,1196298,43,7,13,6,"",,"SHARED2FREE",-1.0766765571584742,"",,"0out_0in","school",105234772 +2566702,"othdiscr",1,1,1,1,"non_mandatory",1,9000,25000,1196298,171,18,20,2,"",14.120222605326392,"SHARED2FREE",0.3431222104372727,"",,"0out_2in","othdiscr",105234807 +2936848,"eatout",1,1,3,3,"non_mandatory",1,22000,11000,1286557,128,13,17,4,"",15.413580157739286,"WALK",3.337722927621502,"",,"0out_0in","eatout",120410774 +2936848,"othmaint",1,1,2,3,"non_mandatory",1,5000,11000,1286557,59,8,13,5,"",14.936437315067716,"WALK_LOC",1.9387725623593302,"",,"2out_1in","othmaint",120410796 +2936848,"shopping",1,1,1,3,"non_mandatory",1,11000,11000,1286557,170,18,19,1,"",13.737502885247409,"WALK",1.7463445938625892,"",,"0out_2in","shopping",120410801 +3061894,"othmaint",1,1,2,2,"non_mandatory",1,5000,24000,1363467,63,8,17,9,"",15.189637319752025,"WALK_LOC",2.2612689227267926,"",,"0out_0in","othmaint",125537682 +3061894,"shopping",1,1,1,2,"non_mandatory",1,5000,24000,1363467,54,8,8,0,"",13.926378681444465,"TNC_SINGLE",2.195783573785629,"",,"1out_1in","shopping",125537687 +3061895,"othmaint",1,1,2,2,"non_mandatory",1,22000,24000,1363467,180,20,20,0,"",15.281994100444798,"WALK_LOC",2.3241680009556887,"",,"0out_0in","othmaint",125537723 +3061895,"shopping",1,1,1,2,"non_mandatory",1,4000,24000,1363467,66,8,20,12,"",13.894949372888025,"TNC_SINGLE",2.349852517486852,"",,"1out_0in","shopping",125537728 +3188483,"othmaint",1,1,2,2,"non_mandatory",1,3000,25000,1402945,112,12,12,0,"",14.231882110325735,"WALK",1.3527690062199011,"",,"0out_0in","othmaint",130727831 +3188483,"shopping",1,1,1,2,"non_mandatory",1,14000,25000,1402945,136,14,15,1,"",13.406265391553307,"BIKE",0.9749240595315471,"",,"0out_0in","shopping",130727836 +3188484,"work",1,1,1,1,"mandatory",1,21000,25000,1402945,147,15,17,2,"",,"SHARED2FREE",1.5045736701182255,"no_subtours",,"0out_0in","work",130727883 +3188485,"work",1,1,1,1,"mandatory",1,5000,25000,1402945,64,8,18,10,"",,"WALK",2.034000565768078,"no_subtours",,"0out_0in","work",130727924 +3232955,"escort",1,1,1,1,"non_mandatory",1,7000,14000,1444715,164,17,19,2,"",12.435690330338216,"WALK",-2.0590794826886896,"",,"0out_0in","escort",132551164 +3232955,"work",2,1,1,2,"mandatory",1,22000,14000,1444715,24,6,11,5,"",,"WALK",-0.40159974247061203,"no_subtours",,"0out_0in","work",132551194 +3232955,"work",2,2,2,2,"mandatory",1,22000,14000,1444715,127,13,16,3,"",,"WALK_LOC",-0.20548633109849337,"no_subtours",,"0out_0in","work",132551195 +3233462,"eat",1,1,1,1,"atwork",1,5000,19000,1445222,70,9,9,0,"",19.63149392560022,"SHARED3FREE",-0.34159841664366153,"",132571981,"0out_1in","atwork",132571946 +3233462,"work",1,1,1,1,"mandatory",1,19000,17000,1445222,81,9,20,11,"",,"DRIVEALONEFREE",0.16481970561443188,"eat",,"0out_3in","work",132571981 +3328568,"work",1,1,1,1,"mandatory",1,22000,8000,1511234,46,7,16,9,"",,"WALK_LRF",6.0835517322893065,"no_subtours",,"0out_0in","work",136471327 +3328569,"school",1,1,1,1,"mandatory",1,9000,8000,1511234,62,8,16,8,"",,"WALK_LOC",7.510086352530541,"",,"0out_0in","univ",136471360 +3495342,"eat",1,1,1,1,"atwork",1,9000,11000,1594621,85,10,10,0,"",15.668488627428035,"WALK",5.895907678328029,"",143309061,"3out_0in","atwork",143309026 +3495342,"work",1,1,1,1,"mandatory",1,11000,10000,1594621,63,8,17,9,"",,"TNC_SINGLE",6.106308966698332,"eat",,"0out_0in","work",143309061 +3495343,"shopping",1,1,1,1,"non_mandatory",1,21000,10000,1594621,146,15,16,1,"",14.147994174455755,"TAXI",2.140539157970799,"",,"1out_1in","shopping",143309096 +3596364,"school",1,1,1,1,"mandatory",1,9000,9000,1645132,99,11,11,0,"",,"WALK",0.9922761728862803,"",,"0out_0in","univ",147450955 +3596364,"shopping",1,1,1,1,"non_mandatory",1,2000,9000,1645132,130,13,19,6,"",12.702108843408501,"WALK_LRF",-0.6357173968922101,"",,"0out_0in","shopping",147450957 +3596365,"school",1,1,1,1,"mandatory",1,9000,9000,1645132,92,10,17,7,"",,"WALK",0.13078470986223545,"",,"0out_2in","school",147450996 +3891102,"eat",1,1,1,1,"atwork",1,4000,1000,1747467,88,10,13,3,"",12.863034889266403,"WALK",0.48410709631297644,"",159535221,"0out_1in","atwork",159535186 +3891102,"work",1,1,1,1,"mandatory",1,1000,16000,1747467,67,8,21,13,"",,"WALK_LOC",1.7047589669241154,"eat",,"1out_1in","work",159535221 +3891104,"othdiscr",1,1,1,1,"non_mandatory",1,17000,16000,1747467,52,7,22,15,"",14.783602512881732,"WALK",1.8681070245632654,"",,"0out_0in","othdiscr",159535289 +4171615,"school",1,1,1,1,"mandatory",1,13000,16000,1810015,169,18,18,0,"",,"TAXI",3.339440236713284,"",,"0out_0in","univ",171036246 +4171616,"shopping",1,1,1,1,"non_mandatory",1,14000,16000,1810015,89,10,14,4,"",13.351914976059247,"WALK",1.2336467654702536,"",,"0out_0in","shopping",171036289 +4171617,"eat",1,1,1,1,"atwork",1,5000,11000,1810015,85,10,10,0,"",12.806283314422718,"WALK",0.1414193868413481,"",171036336,"0out_1in","atwork",171036301 +4171617,"work",1,1,1,1,"mandatory",1,11000,16000,1810015,62,8,16,8,"",,"WALK",1.2491275426711392,"eat",,"0out_0in","work",171036336 +4171619,"othdiscr",1,1,1,1,"non_mandatory",1,16000,16000,1810015,80,9,19,10,"",14.427951929207534,"WALK",1.7126466601147134,"",,"0out_0in","othdiscr",171036404 +4171622,"othmaint",1,1,1,1,"non_mandatory",1,2000,16000,1810015,100,11,12,1,"",14.02155021495475,"TNC_SINGLE",0.47701959308489095,"",,"0out_0in","othmaint",171036530 +4823797,"work",1,1,1,1,"mandatory",1,2000,14000,1952792,93,10,18,8,"",,"WALK",5.550859155970048,"no_subtours",,"0out_0in","work",197775716 +5057160,"work",1,1,1,1,"mandatory",1,5000,5000,2048204,30,6,17,11,"",,"BIKE",-0.09630302951387847,"no_subtours",,"0out_0in","work",207343599 +5057338,"work",1,1,1,1,"mandatory",1,17000,7000,2048382,50,7,20,13,"",,"WALK_LOC",5.537496365437239,"no_subtours",,"0out_0in","work",207350897 +5387762,"work",1,1,1,1,"mandatory",1,10000,9000,2223027,28,6,15,9,"",,"WALK",1.9810584290306386,"no_subtours",,"0out_0in","work",220898281 +5387763,"eatout",1,1,2,2,"non_mandatory",1,10000,9000,2223027,154,16,16,0,"",14.015959650256292,"WALK",2.3358373327911104,"",,"0out_0in","eatout",220898289 +5387763,"othdiscr",1,1,1,2,"non_mandatory",1,15000,9000,2223027,169,18,18,0,"",14.599617247497788,"WALK_LRF",1.465646852694868,"",,"0out_0in","othdiscr",220898308 +5389226,"work",1,1,1,1,"mandatory",1,14000,16000,2223759,63,8,17,9,"",,"WALK",1.8883570091287778,"no_subtours",,"0out_0in","work",220958305 +5389227,"eat",1,1,1,1,"atwork",1,16000,16000,2223759,99,11,11,0,"",12.858228927386488,"WALK",0.6660642166270728,"",220958346,"0out_0in","atwork",220958311 +5389227,"escort",1,1,1,1,"non_mandatory",1,5000,16000,2223759,162,17,17,0,"",12.681412371417782,"WALK",-0.2552610926701476,"",,"0out_0in","escort",220958316 +5389227,"work",1,1,1,1,"mandatory",1,16000,16000,2223759,28,6,15,9,"",,"WALK",2.079861874799809,"eat",,"0out_0in","work",220958346 +7305540,"social",2,1,1,2,"non_mandatory",1,21000,20000,2727273,37,7,7,0,"",14.1996409773108,"WALK",1.8494027479889232,"",,"0out_0in","social",299527176 +7305540,"social",2,2,2,2,"non_mandatory",1,9000,20000,2727273,86,10,11,1,"",14.120642597671408,"WALK",1.9956840012673436,"",,"0out_1in","social",299527177 +7305540,"work",1,1,1,1,"mandatory",1,24000,20000,2727273,127,13,16,3,"",,"BIKE",0.9398542864235182,"no_subtours",,"0out_0in","work",299527179 +7305541,"shopping",1,1,1,2,"non_mandatory",1,16000,20000,2727273,171,18,20,2,"",13.28794064279901,"WALK_LOC",0.9142001643337417,"",,"0out_0in","shopping",299527214 +7305541,"social",1,1,2,2,"non_mandatory",1,21000,20000,2727273,162,17,17,0,"",14.151071365958824,"WALK_LOC",1.8493576999144647,"",,"0out_0in","social",299527217 +7305541,"work",1,1,1,1,"mandatory",1,4000,20000,2727273,45,7,15,8,"",,"WALK_LRF",1.6475450149587834,"no_subtours",,"0out_0in","work",299527220 +7453413,"othmaint",1,1,1,1,"non_mandatory",1,8000,20000,2762078,102,11,14,3,"",14.985169483093204,"WALK_LOC",2.1409730693117166,"",,"0out_0in","othmaint",305589961 +7511873,"work",1,1,1,1,"mandatory",1,1000,8000,2820538,45,7,15,8,"",,"WALK_LOC",-0.8702887383817772,"no_subtours",,"0out_0in","work",307986832 +7512109,"work",1,1,1,1,"mandatory",1,14000,8000,2820774,48,7,18,11,"",,"WALK_LOC",4.688080695799631,"no_subtours",,"0out_0in","work",307996508 +7512514,"work",1,1,1,1,"mandatory",1,9000,8000,2821179,172,18,21,3,"",,"WALK",5.16970893935428,"no_subtours",,"0out_0in","work",308013113 +7513432,"social",1,1,1,1,"non_mandatory",1,11000,8000,2822097,77,9,16,7,"",14.426345007668951,"WALK_LOC",1.828284029776891,"",,"0out_1in","social",308050748 +7513554,"work",1,1,1,1,"mandatory",1,9000,8000,2822219,96,10,21,11,"",,"TNC_SINGLE",5.634094342054246,"no_subtours",,"1out_0in","work",308055753 +7523517,"shopping",1,1,1,1,"non_mandatory",1,13000,7000,2832182,145,15,15,0,"",13.532091345687146,"WALK_LOC",1.2623216832302828,"",,"0out_0in","shopping",308464230 diff --git a/activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_trips_2_zone.csv b/activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_trips_2_zone.csv new file mode 100644 index 0000000000..bd9250049b --- /dev/null +++ b/activitysim/examples/placeholder_multiple_zone/test/regress/final_eet_trips_2_zone.csv @@ -0,0 +1,254 @@ +"person_id","household_id","primary_purpose","trip_num","outbound","trip_count","destination","origin","tour_id","purpose","destination_logsum","depart","trip_mode","mode_choice_logsum","trip_id" +26686,26686,"shopping",1,true,1,23000,8000,1094159,"shopping",,12,"WALK_LOC",1.9827280669801086,8753273 +26686,26686,"shopping",1,false,1,8000,23000,1094159,"home",,13,"WALK",1.9037282242699007,8753277 +26844,26844,"othmaint",1,true,1,5000,8000,1100632,"othmaint",,16,"WALK",3.9354602248477737,8805057 +26844,26844,"othmaint",1,false,1,8000,5000,1100632,"home",,21,"WALK",3.9189147972940885,8805061 +26844,26844,"shopping",1,true,2,9000,8000,1100637,"shopping",31.37877712789407,9,"WALK_LOC",10.984274105698468,8805097 +26844,26844,"shopping",2,true,2,1000,9000,1100637,"shopping",,9,"TNC_SINGLE",1.1046643197551034,8805098 +26844,26844,"shopping",1,false,1,8000,1000,1100637,"home",,14,"TNC_SINGLE",0.9942441548836948,8805101 +27726,27726,"eatout",1,true,1,10000,10000,1136772,"eatout",,14,"WALK",10.041713156072243,9094177 +27726,27726,"eatout",1,false,1,10000,10000,1136772,"home",,19,"WALK",10.041713156072243,9094181 +110675,110675,"work",1,true,1,16000,16000,4537714,"work",,5,"WALK",5.450623197234638,36301713 +110675,110675,"work",1,false,1,16000,16000,4537714,"home",,18,"WALK",5.450623197234638,36301717 +112064,112064,"work",1,true,2,25000,16000,4594663,"work",35.8782468879873,13,"WALK_LOC",10.001126639886184,36757305 +112064,112064,"work",2,true,2,24000,25000,4594663,"work",,13,"WALK_LOC",2.809733442795069,36757306 +112064,112064,"work",1,false,2,7000,24000,4594663,"social",34.01849775944516,20,"WALK",2.147976347182729,36757309 +112064,112064,"work",2,false,2,16000,7000,4594663,"home",,20,"WALK_LOC",10.657184142644127,36757310 +264108,226869,"eatout",1,true,1,8000,9000,10828434,"eatout",,14,"WALK",12.045414087419827,86627473 +264108,226869,"eatout",1,false,1,9000,8000,10828434,"home",,14,"WALK",12.045414087420168,86627477 +323689,256660,"work",1,true,2,7000,10000,13271288,"work",32.636430904267925,15,"WALK",10.992779238372787,106170305 +323689,256660,"work",2,true,2,2000,7000,13271288,"work",,16,"WALK",0.07082651725982507,106170306 +323689,256660,"work",1,false,1,10000,2000,13271288,"home",,21,"WALK_LRF",0.4297270030393883,106170309 +323690,256660,"work",1,true,1,9000,10000,13271329,"work",,12,"WALK",8.030042498532831,106170633 +323690,256660,"work",1,false,2,9000,9000,13271329,"escort",41.699169508847675,16,"WALK",8.311642517531904,106170637 +323690,256660,"work",2,false,2,10000,9000,13271329,"home",,17,"WALK",8.170842512701038,106170638 +325431,257531,"othdiscr",1,true,1,22000,16000,13342696,"othdiscr",,11,"WALK",0.8780862049084773,106741569 +325431,257531,"othdiscr",1,false,3,8000,22000,13342696,"social",31.597406256890405,14,"WALK",-0.20976456497029897,106741573 +325431,257531,"othdiscr",2,false,3,7000,8000,13342696,"escort",55.615835217285294,14,"WALK",12.357894047797622,106741574 +325431,257531,"othdiscr",3,false,3,16000,7000,13342696,"home",,14,"WALK",12.573466152605935,106741575 +325431,257531,"work",1,true,1,14000,16000,13342710,"work",,16,"WALK_LOC",1.566089725814676,106741681 +325431,257531,"work",1,false,1,16000,14000,13342710,"home",,19,"WALK",1.5222029212145018,106741685 +325432,257531,"work",1,true,1,15000,16000,13342751,"work",,7,"WALK_LOC",1.5021356653376314,106742009 +325432,257531,"work",1,false,1,16000,15000,13342751,"home",,15,"WALK",1.4521916317367,106742013 +595684,370497,"escort",1,true,1,5000,21000,24423053,"escort",,7,"WALK",3.330906385141443,195384425 +595684,370497,"escort",1,false,1,21000,5000,24423053,"home",,8,"WALK",3.406854952912418,195384429 +595684,370497,"work",1,true,4,8000,21000,24423083,"shopping",26.255350098408414,17,"WALK",9.135886955872488,195384665 +595684,370497,"work",2,true,4,9000,8000,24423083,"work",28.05855070184786,18,"WALK",8.121041430209859,195384666 +595684,370497,"work",3,true,4,7000,9000,24423083,"work",31.847339393333378,18,"WALK",10.660973323175552,195384667 +595684,370497,"work",4,true,4,19000,7000,24423083,"work",,19,"WALK",-0.2788507765426208,195384668 +595684,370497,"work",1,false,1,21000,19000,24423083,"home",,22,"SHARED2FREE",-0.1573937557477881,195384669 +595685,370497,"school",1,true,1,13000,21000,24423116,"school",,8,"WALK",-0.8229268584487489,195384929 +595685,370497,"school",1,false,1,21000,13000,24423116,"home",,15,"WALK_LOC",-1.1046192102021704,195384933 +595686,370497,"school",1,true,1,21000,21000,24423157,"school",,7,"WALK",3.8962741508493144,195385257 +595686,370497,"school",1,false,1,21000,21000,24423157,"home",,11,"WALK",3.8962741508493144,195385261 +644292,386699,"school",1,true,1,2000,7000,26416003,"school",,5,"WALK",-0.7452680126547789,211328025 +644292,386699,"school",1,false,1,7000,2000,26416003,"home",,14,"WALK",-0.6585530704989658,211328029 +644476,386761,"work",1,true,1,7000,16000,26423555,"work",,7,"WALK_LOC",10.956012027742537,211388441 +644476,386761,"work",1,false,1,16000,7000,26423555,"home",,17,"WALK_LOC",10.843966812429903,211388445 +644477,386761,"work",1,true,1,4000,16000,26423596,"work",,8,"WALK",0.5554280270852756,211388769 +644477,386761,"work",1,false,3,8000,4000,26423596,"othdiscr",29.4847474381881,8,"WALK_LOC",0.39972663182606294,211388773 +644477,386761,"work",2,false,3,7000,8000,26423596,"shopping",47.722440883077674,18,"WALK_LOC",10.251476771714287,211388774 +644477,386761,"work",3,false,3,16000,7000,26423596,"home",,18,"WALK",10.843966918146847,211388775 +644478,386761,"school",1,true,1,25000,16000,26423629,"school",,7,"WALK_LOC",11.66210546356803,211389033 +644478,386761,"school",1,false,1,16000,25000,26423629,"home",,15,"WALK_LOC",11.638857607037355,211389037 +1267567,570454,"eatout",1,true,1,11000,21000,51970253,"eatout",,11,"WALK",4.940793809090325,415762025 +1267567,570454,"eatout",1,false,1,21000,11000,51970253,"home",,11,"WALK",4.9407938105296125,415762029 +1427193,703381,"shopping",1,true,1,16000,25000,58514946,"shopping",,15,"BIKE",6.640136269009802,468119569 +1427193,703381,"shopping",1,false,1,25000,16000,58514946,"home",,21,"BIKE",6.575179313692486,468119573 +1427194,703381,"othmaint",1,true,1,9000,25000,58514982,"othmaint",,9,"BIKE",6.353308735567675,468119857 +1427194,703381,"othmaint",1,false,1,25000,9000,58514982,"home",,13,"BIKE",6.3384404915582335,468119861 +1427194,703381,"othmaint",1,true,1,8000,25000,58514983,"othmaint",,16,"WALK",7.518240674377766,468119865 +1427194,703381,"othmaint",1,false,1,25000,8000,58514983,"home",,18,"BIKE",7.524697959231204,468119869 +1427194,703381,"othmaint",1,true,1,7000,25000,58514984,"othmaint",,18,"WALK",8.966945568005901,468119873 +1427194,703381,"othmaint",1,false,1,25000,7000,58514984,"home",,21,"BIKE",8.89909382789557,468119877 +1572659,763879,"othdiscr",1,true,1,7000,6000,64479044,"othdiscr",,5,"WALK",14.202826252443442,515832353 +1572659,763879,"othdiscr",1,false,1,6000,7000,64479044,"home",,13,"WALK",14.258626224164276,515832357 +1572930,764150,"eatout",1,true,1,12000,9000,64490136,"eatout",,7,"WALK",3.8127908930347663,515921089 +1572930,764150,"eatout",1,false,1,9000,12000,64490136,"home",,16,"WALK",3.7569912975474886,515921093 +1632206,823426,"work",1,true,1,12000,11000,66920485,"work",,7,"WALK",3.428915566265564,535363881 +1632206,823426,"work",1,false,1,11000,12000,66920485,"home",,18,"WALK",3.428915026154219,535363885 +1632281,823501,"work",1,true,1,1000,12000,66923560,"work",,8,"WALK",-1.1863064685276732,535388481 +1632281,823501,"work",1,false,1,12000,1000,66923560,"home",,18,"WALK",-1.2019248842710846,535388485 +1632987,824207,"atwork",1,true,1,13000,22000,66952471,"atwork",,11,"TNC_SINGLE",1.1982264470387332,535619769 +1632987,824207,"atwork",1,false,1,22000,13000,66952471,"work",,12,"WALK_LOC",1.1895157253486617,535619773 +1632987,824207,"work",1,true,1,22000,18000,66952506,"work",,7,"WALK_LRF",1.6456092779546538,535620049 +1632987,824207,"work",1,false,1,18000,22000,66952506,"home",,20,"WALK_LRF",1.7361065950351384,535620053 +1875721,982875,"work",1,true,1,4000,16000,76904600,"work",,7,"DRIVEALONEFREE",0.7076921143748968,615236801 +1875721,982875,"work",1,false,1,16000,4000,76904600,"home",,19,"WALK",-0.14334258561745436,615236805 +1875722,982875,"work",1,true,1,7000,16000,76904641,"work",,7,"WALK",9.804561421193789,615237129 +1875722,982875,"work",1,false,1,16000,7000,76904641,"home",,18,"WALK",9.584561383233053,615237133 +2159057,1099626,"work",1,true,1,11000,20000,88521376,"work",,7,"BIKE",3.40833863532139,708171009 +2159057,1099626,"work",1,false,1,20000,11000,88521376,"home",,17,"BIKE",3.370476902119967,708171013 +2159058,1099626,"univ",1,true,1,12000,20000,88521409,"univ",,7,"WALK_LOC",4.673210978795487,708171273 +2159058,1099626,"univ",1,false,1,20000,12000,88521409,"home",,14,"WALK_LOC",4.633766064553818,708171277 +2159059,1099626,"school",1,true,1,17000,20000,88521450,"school",,8,"WALK",2.5208933659011428,708171601 +2159059,1099626,"school",1,false,1,20000,17000,88521450,"home",,15,"WALK",2.4894161638956325,708171605 +2458500,1173905,"othdiscr",1,true,1,2000,8000,100798525,"othdiscr",,13,"WALK_LOC",-0.5086365848499728,806388201 +2458500,1173905,"othdiscr",1,false,1,8000,2000,100798525,"home",,15,"WALK_LOC",-0.5202541824471008,806388205 +2458502,1173905,"school",1,true,1,9000,8000,100798613,"school",,9,"WALK_LOC",9.584957165143996,806388905 +2458502,1173905,"school",1,false,1,8000,9000,100798613,"home",,15,"WALK",9.584141345328328,806388909 +2458503,1173905,"school",1,true,1,25000,8000,100798654,"school",,8,"WALK",11.502135863745265,806389233 +2458503,1173905,"school",1,false,1,8000,25000,100798654,"home",,17,"WALK",11.47503591488948,806389237 +2566698,1196298,"othmaint",1,true,1,12000,25000,105234646,"othmaint",,15,"WALK",2.7170806557365403,841877169 +2566698,1196298,"othmaint",1,false,1,25000,12000,105234646,"home",,16,"WALK",2.668082316887066,841877173 +2566698,1196298,"work",1,true,2,7000,25000,105234657,"work",46.368886774827516,7,"WALK",10.628886549023708,841877257 +2566698,1196298,"work",2,true,2,9000,7000,105234657,"work",,8,"WALK",7.921845595405086,841877258 +2566698,1196298,"work",1,false,3,7000,9000,105234657,"work",46.219913256688756,11,"WALK",8.04493677276671,841877261 +2566698,1196298,"work",2,false,3,7000,7000,105234657,"eatout",50.18591828103494,12,"WALK",11.253671604257528,841877262 +2566698,1196298,"work",3,false,3,25000,7000,105234657,"home",,12,"WALK",10.4836804085622,841877263 +2566699,1196298,"escort",1,true,1,9000,25000,105234668,"escort",,8,"WALK",7.464032001764529,841877345 +2566699,1196298,"escort",1,false,1,25000,9000,105234668,"home",,9,"WALK",7.380265288148372,841877349 +2566699,1196298,"escort",1,true,1,2000,25000,105234669,"escort",,12,"WALK",0.42979489937273185,841877353 +2566699,1196298,"escort",1,false,1,25000,2000,105234669,"home",,12,"DRIVEALONEFREE",0.39426362374490925,841877357 +2566699,1196298,"othdiscr",1,true,1,5000,25000,105234684,"othdiscr",,18,"WALK",4.44638450975239,841877473 +2566699,1196298,"othdiscr",1,false,1,25000,5000,105234684,"home",,21,"WALK",4.078106936734674,841877477 +2566699,1196298,"shopping",1,true,1,17000,25000,105234692,"shopping",,9,"WALK_LRF",5.427479653195204,841877537 +2566699,1196298,"shopping",1,false,1,25000,17000,105234692,"home",,10,"WALK_LOC",5.657997593724093,841877541 +2566700,1196298,"school",1,true,1,19000,25000,105234731,"school",,8,"BIKE",-1.581700286838991,841877849 +2566700,1196298,"school",1,false,1,25000,19000,105234731,"home",,15,"BIKE",-1.6089370260217823,841877853 +2566701,1196298,"escort",1,true,1,13000,25000,105234750,"escort",,13,"SHARED2FREE",-0.03166986394959175,841878001 +2566701,1196298,"escort",1,false,1,25000,13000,105234750,"home",,13,"SHARED2FREE",-0.05985983186201032,841878005 +2566701,1196298,"school",1,true,1,11000,25000,105234772,"school",,7,"WALK",2.362709080528027,841878177 +2566701,1196298,"school",1,false,1,25000,11000,105234772,"home",,13,"WALK",2.1942892427799245,841878181 +2566702,1196298,"othdiscr",1,true,1,9000,25000,105234807,"othdiscr",,18,"WALK",8.443967865601556,841878457 +2566702,1196298,"othdiscr",1,false,3,8000,9000,105234807,"othdiscr",50.54253049075866,20,"WALK",10.530636255741943,841878461 +2566702,1196298,"othdiscr",2,false,3,7000,8000,105234807,"escort",57.021060151258915,20,"WALK",12.331097986979803,841878462 +2566702,1196298,"othdiscr",3,false,3,25000,7000,105234807,"home",,20,"WALK",13.411567550566136,841878463 +2936848,1286557,"eatout",1,true,1,22000,11000,120410774,"eatout",,13,"WALK",-0.37744649738261987,963286193 +2936848,1286557,"eatout",1,false,1,11000,22000,120410774,"home",,17,"WALK",-0.12636613590700893,963286197 +2936848,1286557,"othmaint",1,true,3,25000,11000,120410796,"othmaint",33.53386230240779,8,"WALK_LOC",8.71506906881393,963286369 +2936848,1286557,"othmaint",2,true,3,7000,25000,120410796,"othmaint",35.15663604383877,9,"WALK_LOC",9.599397367555662,963286370 +2936848,1286557,"othmaint",3,true,3,5000,7000,120410796,"othmaint",,9,"WALK",3.8856993443502486,963286371 +2936848,1286557,"othmaint",1,false,2,8000,5000,120410796,"shopping",32.12692001802512,13,"WALK_LOC",3.8136019897435065,963286373 +2936848,1286557,"othmaint",2,false,2,11000,8000,120410796,"home",,13,"WALK",8.624638911562878,963286374 +2936848,1286557,"shopping",1,true,1,11000,11000,120410801,"shopping",,18,"WALK",5.186311186707391,963286409 +2936848,1286557,"shopping",1,false,3,7000,11000,120410801,"othdiscr",44.33949605364542,18,"WALK",4.706431251058834,963286413 +2936848,1286557,"shopping",2,false,3,7000,7000,120410801,"escort",61.6886580104208,19,"WALK",14.414866203326692,963286414 +2936848,1286557,"shopping",3,false,3,11000,7000,120410801,"home",,19,"WALK",13.856866219908829,963286415 +3061894,1363467,"othmaint",1,true,1,5000,24000,125537682,"othmaint",,8,"WALK_LOC",3.73725010300736,1004301457 +3061894,1363467,"othmaint",1,false,1,24000,5000,125537682,"home",,17,"WALK",3.7305842343863196,1004301461 +3061894,1363467,"shopping",1,true,2,7000,24000,125537687,"othmaint",43.73819861241678,8,"WALK_LOC",13.495011906378089,1004301497 +3061894,1363467,"shopping",2,true,2,5000,7000,125537687,"shopping",,8,"WALK_LOC",4.355163009411816,1004301498 +3061894,1363467,"shopping",1,false,2,9000,5000,125537687,"eatout",42.95816610580615,8,"WALK_LOC",4.348166044302983,1004301501 +3061894,1363467,"shopping",2,false,2,24000,9000,125537687,"home",,8,"WALK_HVY",10.804085780654413,1004301502 +3061895,1363467,"othmaint",1,true,1,22000,24000,125537723,"othmaint",,20,"WALK",2.1198382474364728,1004301785 +3061895,1363467,"othmaint",1,false,1,24000,22000,125537723,"home",,20,"WALK",2.2450248366859453,1004301789 +3061895,1363467,"shopping",1,true,2,25000,24000,125537728,"shopping",35.38452370877508,8,"WALK_LOC",12.887343785241573,1004301825 +3061895,1363467,"shopping",2,true,2,4000,25000,125537728,"shopping",,11,"WALK_LOC",0.8165311300377738,1004301826 +3061895,1363467,"shopping",1,false,1,24000,4000,125537728,"home",,20,"WALK_LOC",0.8266547678720484,1004301829 +3188483,1402945,"othmaint",1,true,1,3000,25000,130727831,"othmaint",,12,"WALK",6.049470656726249,1045822649 +3188483,1402945,"othmaint",1,false,1,25000,3000,130727831,"home",,12,"WALK",6.009290681658803,1045822653 +3188483,1402945,"shopping",1,true,1,14000,25000,130727836,"shopping",,14,"BIKE",0.6559860145760328,1045822689 +3188483,1402945,"shopping",1,false,1,25000,14000,130727836,"home",,15,"BIKE",0.5716281146927128,1045822693 +3188484,1402945,"work",1,true,1,21000,25000,130727883,"work",,15,"DRIVEALONEFREE",2.2611057836224124,1045823065 +3188484,1402945,"work",1,false,1,25000,21000,130727883,"home",,17,"WALK",1.79496008180406,1045823069 +3188485,1402945,"work",1,true,1,5000,25000,130727924,"work",,8,"WALK",3.176125996500932,1045823393 +3188485,1402945,"work",1,false,1,25000,5000,130727924,"home",,18,"WALK",2.885731258806822,1045823397 +3232955,1444715,"escort",1,true,1,7000,14000,132551164,"escort",,17,"WALK",13.438366259007205,1060409313 +3232955,1444715,"escort",1,false,1,14000,7000,132551164,"home",,19,"WALK",13.187266160073705,1060409317 +3232955,1444715,"work",1,true,1,22000,14000,132551194,"work",,6,"WALK",0.5603936172504794,1060409553 +3232955,1444715,"work",1,false,1,14000,22000,132551194,"home",,11,"WALK",0.8331864635175756,1060409557 +3232955,1444715,"work",1,true,1,22000,14000,132551195,"work",,13,"WALK",1.649789344008908,1060409561 +3232955,1444715,"work",1,false,1,14000,22000,132551195,"home",,16,"WALK_LOC",1.802140999072509,1060409565 +3233462,1445222,"atwork",1,true,1,5000,19000,132571946,"atwork",,9,"WALK",3.555384833111897,1060575569 +3233462,1445222,"atwork",1,false,2,10000,5000,132571946,"work",40.81104535324782,9,"WALK",5.09581492505858,1060575573 +3233462,1445222,"atwork",2,false,2,19000,10000,132571946,"work",,9,"WALK",10.432166529592788,1060575574 +3233462,1445222,"work",1,true,1,19000,17000,132571981,"work",,9,"DRIVEALONEFREE",-0.12604572730314542,1060575849 +3233462,1445222,"work",1,false,4,7000,19000,132571981,"work",26.29280923831885,17,"DRIVEALONEFREE",-0.7072010889663861,1060575853 +3233462,1445222,"work",2,false,4,6000,7000,132571981,"othmaint",42.469375553808376,17,"WALK",10.073578746267579,1060575854 +3233462,1445222,"work",3,false,4,7000,6000,132571981,"escort",40.40244821152917,17,"WALK",8.854601045166811,1060575855 +3233462,1445222,"work",4,false,4,17000,7000,132571981,"home",,20,"WALK",8.238896211056357,1060575856 +3328568,1511234,"work",1,true,1,22000,8000,136471327,"work",,7,"WALK_LRF",2.0129407133763646,1091770617 +3328568,1511234,"work",1,false,1,8000,22000,136471327,"home",,16,"WALK_LRF",2.013498720638361,1091770621 +3328569,1511234,"univ",1,true,1,9000,8000,136471360,"univ",,8,"WALK_LOC",10.078747551647542,1091770881 +3328569,1511234,"univ",1,false,1,8000,9000,136471360,"home",,16,"WALK_LOC",10.077775953315786,1091770885 +3495342,1594621,"atwork",1,true,4,9000,11000,143309026,"escort",52.586546235089074,10,"WALK",10.660093773427764,1146472209 +3495342,1594621,"atwork",2,true,4,7000,9000,143309026,"eatout",54.81382092908523,10,"WALK",14.040826305510333,1146472210 +3495342,1594621,"atwork",3,true,4,9000,7000,143309026,"eatout",52.150762754342416,10,"WALK",10.50217377147708,1146472211 +3495342,1594621,"atwork",4,true,4,9000,9000,143309026,"atwork",,10,"WALK",11.021053654094953,1146472212 +3495342,1594621,"atwork",1,false,1,11000,9000,143309026,"work",,10,"WALK",10.675133776355615,1146472213 +3495342,1594621,"work",1,true,1,11000,10000,143309061,"work",,8,"WALK",4.542307812891537,1146472489 +3495342,1594621,"work",1,false,1,10000,11000,143309061,"home",,17,"WALK",4.540040319156421,1146472493 +3495343,1594621,"shopping",1,true,2,8000,10000,143309096,"eatout",44.07715065746949,15,"WALK_LOC",12.334958131287545,1146472769 +3495343,1594621,"shopping",2,true,2,21000,8000,143309096,"shopping",,15,"WALK_LOC",4.754322605614275,1146472770 +3495343,1594621,"shopping",1,false,2,3000,21000,143309096,"shopping",35.64487169509503,16,"WALK_LOC",4.579037479011556,1146472773 +3495343,1594621,"shopping",2,false,2,10000,3000,143309096,"home",,16,"WALK_LRF",9.834778533473422,1146472774 +3596364,1645132,"univ",1,true,1,9000,9000,147450955,"univ",,11,"WALK",10.238432625148134,1179607641 +3596364,1645132,"univ",1,false,1,9000,9000,147450955,"home",,11,"WALK",10.238432625148134,1179607645 +3596364,1645132,"shopping",1,true,1,2000,9000,147450957,"shopping",,13,"WALK_LRF",0.8285401596132209,1179607657 +3596364,1645132,"shopping",1,false,1,9000,2000,147450957,"home",,19,"WALK_LRF",0.8244659836149215,1179607661 +3596365,1645132,"school",1,true,1,9000,9000,147450996,"school",,10,"WALK",10.238432621193224,1179607969 +3596365,1645132,"school",1,false,3,25000,9000,147450996,"shopping",42.28625449659161,17,"WALK",7.7398125409163105,1179607973 +3596365,1645132,"school",2,false,3,6000,25000,147450996,"othmaint",53.965718905093475,17,"WALK",12.120015882476332,1179607974 +3596365,1645132,"school",3,false,3,9000,6000,147450996,"home",,17,"WALK",11.33462804101704,1179607975 +3891102,1747467,"atwork",1,true,1,4000,1000,159535186,"atwork",,10,"WALK",0.45813980969805285,1276281489 +3891102,1747467,"atwork",1,false,2,25000,4000,159535186,"eatout",34.80381527769826,13,"WALK",0.2084762800360644,1276281493 +3891102,1747467,"atwork",2,false,2,1000,25000,159535186,"work",,13,"WALK",13.371530528904229,1276281494 +3891102,1747467,"work",1,true,2,7000,16000,159535221,"escort",30.09854287915041,8,"WALK",11.099112493958854,1276281769 +3891102,1747467,"work",2,true,2,1000,7000,159535221,"work",,10,"WALK",-0.14398137208511666,1276281770 +3891102,1747467,"work",1,false,2,6000,1000,159535221,"shopping",26.473055121441792,17,"WALK",-0.29357283995282485,1276281773 +3891102,1747467,"work",2,false,2,16000,6000,159535221,"home",,21,"WALK_LOC",9.754298157016313,1276281774 +3891104,1747467,"othdiscr",1,true,1,17000,16000,159535289,"othdiscr",,7,"WALK",5.9419631748312804,1276282313 +3891104,1747467,"othdiscr",1,false,1,16000,17000,159535289,"home",,22,"WALK",5.85268329864962,1276282317 +4171615,1810015,"univ",1,true,1,13000,16000,171036246,"univ",,18,"TNC_SHARED",-0.752316656547577,1368289969 +4171615,1810015,"univ",1,false,1,16000,13000,171036246,"home",,18,"WALK_LOC",-0.7196572448668817,1368289973 +4171616,1810015,"shopping",1,true,1,14000,16000,171036289,"shopping",,10,"WALK",1.0944891382847346,1368290313 +4171616,1810015,"shopping",1,false,1,16000,14000,171036289,"home",,14,"WALK",0.9382513291595582,1368290317 +4171617,1810015,"atwork",1,true,1,5000,11000,171036301,"atwork",,10,"WALK",4.630819623213244,1368290409 +4171617,1810015,"atwork",1,false,2,6000,5000,171036301,"escort",44.69367598003321,10,"WALK",4.690979682144286,1368290413 +4171617,1810015,"atwork",2,false,2,11000,6000,171036301,"work",,10,"WALK",12.460449128670621,1368290414 +4171617,1810015,"work",1,true,1,11000,16000,171036336,"work",,8,"WALK",2.7959725074724067,1368290689 +4171617,1810015,"work",1,false,1,16000,11000,171036336,"home",,16,"WALK",2.7959727582005645,1368290693 +4171619,1810015,"othdiscr",1,true,1,16000,16000,171036404,"othdiscr",,9,"WALK",7.330879449163802,1368291233 +4171619,1810015,"othdiscr",1,false,1,16000,16000,171036404,"home",,19,"WALK",7.330879449572421,1368291237 +4171622,1810015,"othmaint",1,true,1,2000,16000,171036530,"othmaint",,11,"TNC_SINGLE",0.05700613810224191,1368292241 +4171622,1810015,"othmaint",1,false,1,16000,2000,171036530,"home",,12,"TNC_SHARED",0.12318961832260422,1368292245 +4823797,1952792,"work",1,true,1,2000,14000,197775716,"work",,10,"WALK",-0.12977069563455434,1582205729 +4823797,1952792,"work",1,false,1,14000,2000,197775716,"home",,18,"WALK",-0.270575833053774,1582205733 +5057160,2048204,"work",1,true,1,5000,5000,207343599,"work",,6,"BIKE",3.570867061441779,1658748793 +5057160,2048204,"work",1,false,1,5000,5000,207343599,"home",,17,"BIKE",3.570867061441779,1658748797 +5057338,2048382,"work",1,true,1,17000,7000,207350897,"work",,7,"WALK",4.42461238620194,1658807177 +5057338,2048382,"work",1,false,1,7000,17000,207350897,"home",,20,"WALK_LOC",4.387261966488915,1658807181 +5387762,2223027,"work",1,true,1,10000,9000,220898281,"work",,6,"WALK",7.4825971749227485,1767186249 +5387762,2223027,"work",1,false,1,9000,10000,220898281,"home",,15,"WALK",7.34179717744091,1767186253 +5387763,2223027,"eatout",1,true,1,10000,9000,220898289,"eatout",,16,"WALK",9.90779315781692,1767186313 +5387763,2223027,"eatout",1,false,1,9000,10000,220898289,"home",,16,"WALK",9.729233148863598,1767186317 +5387763,2223027,"othdiscr",1,true,1,15000,9000,220898308,"othdiscr",,18,"WALK_LRF",2.11214260241268,1767186465 +5387763,2223027,"othdiscr",1,false,1,9000,15000,220898308,"home",,18,"WALK_LRF",2.114487481879299,1767186469 +5389226,2223759,"work",1,true,1,14000,16000,220958305,"work",,8,"WALK",0.5332653083530436,1767666441 +5389226,2223759,"work",1,false,1,16000,14000,220958305,"home",,17,"WALK",0.4100846172156184,1767666445 +5389227,2223759,"atwork",1,true,1,16000,16000,220958311,"atwork",,11,"WALK",7.392759591563246,1767666489 +5389227,2223759,"atwork",1,false,1,16000,16000,220958311,"work",,11,"WALK",7.392759591563246,1767666493 +5389227,2223759,"escort",1,true,1,5000,16000,220958316,"escort",,17,"WALK",4.089263672515711,1767666529 +5389227,2223759,"escort",1,false,1,16000,5000,220958316,"home",,17,"WALK",4.011143862031004,1767666533 +5389227,2223759,"work",1,true,1,16000,16000,220958346,"work",,6,"WALK",5.450624006214329,1767666769 +5389227,2223759,"work",1,false,1,16000,16000,220958346,"home",,15,"WALK",5.450624005397038,1767666773 +7305540,2727273,"social",1,true,1,21000,20000,299527176,"social",,7,"WALK",2.5336186542574204,2396217409 +7305540,2727273,"social",1,false,1,20000,21000,299527176,"home",,7,"WALK",2.3656307848426334,2396217413 +7305540,2727273,"social",1,true,1,9000,20000,299527177,"social",,10,"WALK",6.447035361842409,2396217417 +7305540,2727273,"social",1,false,2,8000,9000,299527177,"eatout",36.15580696689693,11,"WALK",6.6220351635608905,2396217421 +7305540,2727273,"social",2,false,2,20000,8000,299527177,"home",,11,"WALK",7.1913682178766365,2396217422 +7305540,2727273,"work",1,true,1,24000,20000,299527179,"work",,13,"BIKE",1.6028747415427587,2396217433 +7305540,2727273,"work",1,false,1,20000,24000,299527179,"home",,16,"BIKE",1.5355807172674794,2396217437 +7305541,2727273,"shopping",1,true,1,16000,20000,299527214,"shopping",,18,"WALK",7.064914498109314,2396217713 +7305541,2727273,"shopping",1,false,1,20000,16000,299527214,"home",,20,"WALK_LOC",7.1093419615753985,2396217717 +7305541,2727273,"social",1,true,1,21000,20000,299527217,"social",,17,"WALK_LOC",3.0911048477980314,2396217737 +7305541,2727273,"social",1,false,1,20000,21000,299527217,"home",,17,"WALK_LOC",3.200851120839349,2396217741 +7305541,2727273,"work",1,true,1,4000,20000,299527220,"work",,7,"WALK",-0.11437934759116813,2396217761 +7305541,2727273,"work",1,false,1,20000,4000,299527220,"home",,15,"WALK_LRF",0.6663056921108688,2396217765 +7453413,2762078,"othmaint",1,true,1,8000,20000,305589961,"othmaint",,11,"WALK_LOC",8.511850861798528,2444719689 +7453413,2762078,"othmaint",1,false,1,20000,8000,305589961,"home",,14,"WALK",8.452543898989719,2444719693 +7511873,2820538,"work",1,true,1,1000,8000,307986832,"work",,7,"WALK_LOC",-0.5114030615970244,2463894657 +7511873,2820538,"work",1,false,1,8000,1000,307986832,"home",,15,"WALK",-0.6139093584117478,2463894661 +7512109,2820774,"work",1,true,1,14000,8000,307996508,"work",,7,"WALK_LOC",0.6718435845841986,2463972065 +7512109,2820774,"work",1,false,1,8000,14000,307996508,"home",,18,"WALK",0.610566926121762,2463972069 +7512514,2821179,"work",1,true,1,9000,8000,308013113,"work",,18,"WALK",7.994842521447154,2464104905 +7512514,2821179,"work",1,false,1,8000,9000,308013113,"home",,21,"WALK",7.994842521452135,2464104909 +7513432,2822097,"social",1,true,1,11000,8000,308050748,"social",,9,"WALK_LOC",3.706460558335686,2464405985 +7513432,2822097,"social",1,false,2,9000,11000,308050748,"eatout",31.394224343964623,16,"WALK",3.677383696890382,2464405989 +7513432,2822097,"social",2,false,2,8000,9000,308050748,"home",,16,"WALK_LOC",7.113541133018152,2464405990 +7513554,2822219,"work",1,true,2,9000,8000,308055753,"eatout",42.391324918183464,10,"WALK",8.722225942224798,2464446025 +7513554,2822219,"work",2,true,2,9000,9000,308055753,"work",,12,"WALK",8.5974651892884,2464446026 +7513554,2822219,"work",1,false,1,8000,9000,308055753,"home",,21,"WALK",8.698411202168328,2464446029 +7523517,2832182,"shopping",1,true,1,13000,7000,308464230,"shopping",,15,"WALK_LOC",-0.5264083063330212,2467713841 +7523517,2832182,"shopping",1,false,1,7000,13000,308464230,"home",,15,"WALK_LOC",-0.4961048751413181,2467713845 diff --git a/activitysim/examples/placeholder_multiple_zone/test/test_multiple_zone.py b/activitysim/examples/placeholder_multiple_zone/test/test_multiple_zone.py index 3e956301e9..3921bcd0fd 100644 --- a/activitysim/examples/placeholder_multiple_zone/test/test_multiple_zone.py +++ b/activitysim/examples/placeholder_multiple_zone/test/test_multiple_zone.py @@ -39,14 +39,16 @@ def data(): build_data() -def run_test(zone, multiprocess=False): +def run_test(zone, multiprocess=False, use_explicit_error_terms=False): def test_path(dirname): return os.path.join(os.path.dirname(__file__), dirname) - def regress(zone): + def regress(zone, use_explicit_error_terms=False): # regress tours regress_tours_df = pd.read_csv( - test_path(f"regress/final_tours_{zone}_zone.csv") + test_path( + f"regress/final{'_eet' if use_explicit_error_terms else ''}_tours_{zone}_zone.csv" + ) ) tours_df = pd.read_csv(test_path("output/final_tours.csv")) tours_df.to_csv( @@ -59,7 +61,9 @@ def regress(zone): # regress trips regress_trips_df = pd.read_csv( - test_path(f"regress/final_trips_{zone}_zone.csv") + test_path( + f"regress/final{'_eet' if use_explicit_error_terms else ''}_trips_{zone}_zone.csv" + ) ) trips_df = pd.read_csv(test_path("output/final_trips.csv")) trips_df.to_csv( @@ -72,7 +76,15 @@ def regress(zone): file_path = os.path.join(os.path.dirname(__file__), "simulation.py") + test_config_files = [] + if use_explicit_error_terms: + test_config_files = [ + "-c", + test_path("configs_eet"), + ] + run_args = [ + *test_config_files, "-c", test_path(f"configs_{zone}_zone"), "-c", @@ -95,7 +107,7 @@ def regress(zone): else: subprocess.run([sys.executable, file_path] + run_args, check=True) - regress(zone) + regress(zone, use_explicit_error_terms=use_explicit_error_terms) def test_2_zone(data): @@ -106,6 +118,14 @@ def test_2_zone_mp(data): run_test(zone="2", multiprocess=True) +def test_2_zone_eet(data): + run_test(zone="2", multiprocess=False, use_explicit_error_terms=True) + + +def test_2_zone_mp_eet(data): + run_test(zone="2", multiprocess=True, use_explicit_error_terms=True) + + def test_3_zone(data): # python simulation.py -c configs_3_zone -c ../configs_3_zone -c \ # ../../prototype_mtc/configs -d ../data_3 -o output -s settings_mp @@ -202,10 +222,53 @@ def test_path(dirname): print(f"> {zone} zone {step_name}: ok") +@test.run_if_exists("reference_pipeline_2_zone_eet.zip") +def test_multizone_progressive_eet(): + + import activitysim.abm # register components + + def test_path(dirname): + return os.path.join(os.path.dirname(__file__), dirname) + + state = workflow.State.make_default( + configs_dir=( + test_path(f"configs_eet"), + test_path(f"configs_2_zone"), + example_path(f"configs_2_zone"), + mtc_example_path("configs"), + ), + data_dir=(example_path(f"data_2"),), + output_dir=test_path("output"), + settings_file_name="settings.yaml", + ) + + assert state.settings.models == EXPECTED_MODELS + assert state.settings.chunk_size == 0 + assert state.settings.sharrow == False + assert state.settings.use_explicit_error_terms == True + + for step_name in EXPECTED_MODELS: + state.run.by_name(step_name) + try: + state.checkpoint.check_against( + Path(__file__).parent.joinpath("reference_pipeline_2_zone_eet.zip"), + checkpoint_name=step_name, + ) + except Exception: + print(f"> 2 zone eet {step_name}: ERROR") + raise + else: + print(f"> 2 zone {step_name}: ok") + + if __name__ == "__main__": build_data() + run_test(zone="2", multiprocess=False) run_test(zone="2", multiprocess=True) + run_test(zone="2", multiprocess=False, use_explicit_error_terms=True) + run_test(zone="2", multiprocess=True, use_explicit_error_terms=True) + run_test(zone="3", multiprocess=False) run_test(zone="3", multiprocess=True) diff --git a/activitysim/examples/production_semcog/test/configs_eet/settings.yaml b/activitysim/examples/production_semcog/test/configs_eet/settings.yaml new file mode 100644 index 0000000000..dcff83f5a1 --- /dev/null +++ b/activitysim/examples/production_semcog/test/configs_eet/settings.yaml @@ -0,0 +1,5 @@ +inherit_settings: True + +use_explicit_error_terms: True + +rng_base_seed: 42 diff --git a/activitysim/examples/production_semcog/test/regress/final_eet_trips.csv b/activitysim/examples/production_semcog/test/regress/final_eet_trips.csv new file mode 100644 index 0000000000..9826e1d681 --- /dev/null +++ b/activitysim/examples/production_semcog/test/regress/final_eet_trips.csv @@ -0,0 +1,116 @@ +"person_id","household_id","primary_purpose","trip_num","outbound","trip_count","destination","origin","tour_id","purpose","destination_logsum","original_school_zone_id","parked_at_university","depart","tour_includes_parking","trip_id_pre_parking","trip_mode","mode_choice_logsum","trip_id" +2632461,1066212,"eatout",1,true,1,22677,22687,107930907,"eatout",,,false,31,0,863447257,"WALK",1.2747067090732285,1726894513 +2632461,1066212,"eatout",1,false,1,22687,22677,107930907,"home",,,false,36,0,863447261,"WALK",1.2528040978215553,1726894521 +2632461,1066212,"social",1,true,1,22688,22687,107930937,"social",,,false,27,0,863447497,"WALK",0.22160552915226453,1726894993 +2632461,1066212,"social",1,false,1,22687,22688,107930937,"home",,,false,30,0,863447501,"WALK",0.22160552915226453,1726895001 +2632461,1066212,"work",1,true,1,22659,22687,107930940,"work",,,false,11,0,863447521,"DRIVEALONE",-0.2764049012484984,1726895041 +2632461,1066212,"work",1,false,1,22687,22659,107930940,"home",,,false,23,0,863447525,"DRIVEALONE",-0.26321709957638273,1726895049 +2632746,1066390,"school",1,true,2,22689,22688,107942617,"shopping",10.30120931810444,,false,10,0,863540937,"WALK",0.41173295672864374,1727081873 +2632746,1066390,"school",2,true,2,22716,22689,107942617,"school",,,false,10,0,863540938,"SHARED3",-0.12093048344357989,1727081874 +2632746,1066390,"school",1,false,1,22688,22716,107942617,"home",,,false,21,0,863540941,"SHARED3",-0.12094657865851986,1727081881 +2632746,1066390,"work",1,true,1,22688,22688,107942625,"work",,,false,21,0,863541001,"WALK",0.22160552915226453,1727082001 +2632746,1066390,"work",1,false,1,22688,22688,107942625,"home",,,false,26,0,863541005,"WALK",0.22160552915226453,1727082009 +2643231,1070862,"work",1,true,2,22795,22701,108372510,"parking",,,false,12,1,866980081,"DRIVEALONE",-0.8231318335063801,1733960161 +2643231,1070862,"work",2,true,2,22795,22795,108372510,"work",,,true,12,1,866980081,"WALK",2.0056875567057055,1733960162 +2643231,1070862,"work",1,false,2,22795,22795,108372510,"parking",,,true,27,1,866980085,"WALK",2.005696956562539,1733960169 +2643231,1070862,"work",2,false,2,22701,22795,108372510,"home",,,false,27,1,866980085,"DRIVEALONE",-0.856858292126302,1733960170 +2851663,1151807,"work",1,true,1,22743,22768,116918222,"work",,,false,8,0,935345777,"DRIVEALONE",0.32319821567472595,1870691553 +2851663,1151807,"work",1,false,1,22768,22743,116918222,"home",,,false,23,0,935345781,"SHARED2",0.31914343414862156,1870691561 +2851664,1151807,"atwork",1,true,1,22755,22783,116918247,"atwork",,,false,9,0,935345977,"WALK",-0.4285387553231248,1870691953 +2851664,1151807,"atwork",1,false,2,22767,22755,116918247,"eatout",13.620552291309844,,false,9,0,935345981,"WALK",-0.3360993920023427,1870691961 +2851664,1151807,"atwork",2,false,2,22783,22767,116918247,"work",,,false,9,0,935345982,"WALK",2.0196212107066467,1870691962 +2851664,1151807,"work",1,true,2,22783,22768,116918263,"parking",,,false,8,1,935346105,"DRIVEALONE",-0.101763675139766,1870692209 +2851664,1151807,"work",2,true,2,22783,22783,116918263,"work",,,true,8,1,935346105,"WALK",2.394893602362858,1870692210 +2851664,1151807,"work",1,false,2,22783,22783,116918263,"parking",,,true,9,1,935346109,"WALK",2.394893602362858,1870692217 +2851664,1151807,"work",2,false,2,22768,22783,116918263,"home",,,false,9,1,935346109,"DRIVEALONE",-0.14128432072913716,1870692218 +2851664,1151807,"work",1,true,2,22783,22768,116918264,"parking",,,false,20,1,935346113,"SHARED2",0.3036422340987692,1870692225 +2851664,1151807,"work",2,true,2,22783,22783,116918264,"work",,,true,20,1,935346113,"WALK",3.455230939742648,1870692226 +2851664,1151807,"work",1,false,3,22783,22783,116918264,"parking",,,true,22,1,935346117,"WALK",3.455230939742648,1870692233 +2851664,1151807,"work",2,false,3,22743,22783,116918264,"eatout",13.694190728203349,,false,22,1,935346117,"SHARED2",0.12796410019608423,1870692234 +2851664,1151807,"work",3,false,3,22768,22743,116918264,"home",,,false,22,1,935346118,"DRIVEALONE",0.28506746953977524,1870692235 +2851665,1151807,"school",1,true,1,22738,22768,116918296,"school",,,false,9,0,935346369,"WALK",-0.3380929737459932,1870692737 +2851665,1151807,"school",1,false,1,22768,22738,116918296,"home",,,false,25,0,935346373,"WALK",-0.3380929737459932,1870692745 +2851666,1151807,"school",1,true,1,22738,22768,116918337,"school",,,false,9,1,935346697,"WALK",-0.23394837977299351,1870693393 +2851666,1151807,"school",1,false,4,22766,22738,116918337,"parking",,,false,26,1,935346701,"WALK",-0.12281263655431907,1870693401 +2851666,1151807,"school",2,false,4,22767,22766,116918337,"eatout",12.976839556161908,,true,26,1,935346701,"WALK",-0.452052425913061,1870693402 +2851666,1151807,"school",3,false,4,22766,22767,116918337,"parking",,,true,26,1,935346702,"WALK",2.053743433341529,1870693403 +2851666,1151807,"school",4,false,4,22768,22766,116918337,"home",,,false,26,1,935346702,"SHARED3",-0.16257826107609574,1870693404 +2853258,1152693,"work",1,true,1,22738,22767,116983617,"work",,,false,20,0,935868937,"WALK",-0.22675750604679695,1871737873 +2853258,1152693,"work",1,false,1,22767,22738,116983617,"home",,,false,42,0,935868941,"WALK",-0.22675750604679695,1871737881 +2864033,1157863,"work",1,true,1,22801,22818,117425392,"work",,,false,22,0,939403137,"WALK",3.73570922575177,1878806273 +2864033,1157863,"work",1,false,3,22771,22801,117425392,"othmaint",26.926672191384228,,false,43,0,939403141,"WALK",4.180094740047142,1878806281 +2864033,1157863,"work",2,false,3,22767,22771,117425392,"othmaint",27.815811398797507,,false,43,0,939403142,"WALK",4.6038065658867176,1878806282 +2864033,1157863,"work",3,false,3,22818,22767,117425392,"home",,,false,44,0,939403143,"WALK",4.817999024372856,1878806283 +2867650,1159450,"work",1,true,2,22800,22791,117573689,"parking",,,false,12,1,940589513,"DRIVEALONE",-0.06025969651174059,1881179025 +2867650,1159450,"work",2,true,2,22800,22800,117573689,"work",,,true,12,1,940589513,"WALK",3.0701275509879262,1881179026 +2867650,1159450,"work",1,false,2,22800,22800,117573689,"parking",,,true,37,1,940589517,"WALK",3.070138980004933,1881179033 +2867650,1159450,"work",2,false,2,22791,22800,117573689,"home",,,false,37,1,940589517,"WALK",0.2738110299731082,1881179034 +2867652,1159450,"school",1,true,1,22798,22791,117573763,"school",,,false,11,0,940590105,"WALK",-0.1419702876491479,1881180209 +2867652,1159450,"school",1,false,2,22807,22798,117573763,"escort",12.102989575726829,,false,26,0,940590109,"WALK",0.30995293909650445,1881180217 +2867652,1159450,"school",2,false,2,22791,22807,117573763,"home",,,false,27,0,940590110,"WALK",1.1921458680932127,1881180218 +2867653,1159450,"school",1,true,1,22738,22791,117573804,"school",,,false,9,0,940590433,"BIKE",-0.6921067330756006,1881180865 +2867653,1159450,"school",1,false,1,22791,22738,117573804,"home",,,false,23,0,940590437,"BIKE",-0.6921067330756006,1881180873 +2867653,1159450,"school",1,true,1,22738,22791,117573805,"school",,,false,8,0,940590441,"SCHOOLBUS",-1.3378817936541838,1881180881 +2867653,1159450,"school",1,false,1,22791,22738,117573805,"home",,,false,8,0,940590445,"SHARED2",-9.871239947524709,1881180889 +2869308,1160345,"escort",1,true,1,22814,22788,117641637,"escort",,,false,10,0,941133097,"WALK",-0.2948412350381067,1882266193 +2869308,1160345,"escort",1,false,1,22788,22814,117641637,"home",,,false,10,0,941133101,"WALK",-0.2948412350381067,1882266201 +2869308,1160345,"work",1,true,1,22640,22788,117641667,"work",,,false,11,1,941133337,"SHARED2",-0.6362720979256549,1882266673 +2869308,1160345,"work",1,false,6,22761,22640,117641667,"parking",,,false,27,1,941133341,"SHARED3",-0.5969499608294799,1882266681 +2869308,1160345,"work",2,false,6,22767,22761,117641667,"othmaint",11.470504256383801,,true,27,1,941133341,"WALK",3.0222067430139012,1882266682 +2869308,1160345,"work",3,false,6,22761,22767,117641667,"parking",,,true,28,1,941133342,"WALK",4.036268963328661,1882266683 +2869308,1160345,"work",4,false,6,22769,22761,117641667,"shopping",13.481404724590321,,false,28,1,941133342,"WALK",0.4697934390352235,1882266684 +2869308,1160345,"work",5,false,6,22769,22769,117641667,"escort",13.817298583185659,,false,29,1,941133343,"DRIVEALONE",0.5088371121647843,1882266685 +2869308,1160345,"work",6,false,6,22788,22769,117641667,"home",,,false,30,1,941133344,"DRIVEALONE",0.38196512409824096,1882266686 +2869309,1160345,"univ",1,true,2,22795,22788,117641700,"parking",,,false,13,1,941133601,"DRIVEALONE",-0.15235107523409816,1882267201 +2869309,1160345,"univ",2,true,2,22766,22795,117641700,"univ",,,true,13,1,941133601,"WALK_LOC",1.202786557349171,1882267202 +2869309,1160345,"univ",1,false,3,22795,22766,117641700,"parking",,,true,24,1,941133605,"WALK_LOC",1.142188272503556,1882267209 +2869309,1160345,"univ",2,false,3,22769,22795,117641700,"othdiscr",12.456311079956105,,false,24,1,941133605,"WALK",-1.6313849658981006,1882267210 +2869309,1160345,"univ",3,false,3,22788,22769,117641700,"home",,,false,24,1,941133606,"DRIVEALONE",-0.3087846946902839,1882267211 +2869392,1160408,"shopping",1,true,2,22797,22784,117645105,"parking",,,false,26,1,941160841,"DRIVEALONE",-0.0973453493011812,1882321681 +2869392,1160408,"shopping",2,true,2,22767,22797,117645105,"shopping",,,true,26,1,941160841,"WALK",3.635953706352258,1882321682 +2869392,1160408,"shopping",1,false,3,22797,22767,117645105,"parking",,,true,36,1,941160845,"WALK",3.2319057420708,1882321689 +2869392,1160408,"shopping",2,false,3,22778,22797,117645105,"othmaint",13.169263985923191,,false,36,1,941160845,"DRIVEALONE",-0.8388301157218331,1882321690 +2869392,1160408,"shopping",3,false,3,22784,22778,117645105,"home",,,false,37,1,941160846,"DRIVEALONE",-0.21796488394191485,1882321691 +2871041,1161101,"work",1,true,1,22801,22747,117712720,"work",,,false,10,0,941701761,"PNR_LOC",0.001366053793971812,1883403521 +2871041,1161101,"work",1,false,1,22747,22801,117712720,"home",,,false,30,0,941701765,"PNR_LOC",-0.0004743815570571668,1883403529 +2871042,1161101,"work",1,true,2,22802,22747,117712761,"parking",,,false,6,1,941702089,"DRIVEALONE",0.31437493739186884,1883404177 +2871042,1161101,"work",2,true,2,22802,22802,117712761,"work",,,true,6,1,941702089,"WALK",3.98103278438962,1883404178 +2871042,1161101,"work",1,false,2,22802,22802,117712761,"parking",,,true,31,1,941702093,"WALK",3.9810287626204213,1883404185 +2871042,1161101,"work",2,false,2,22747,22802,117712761,"home",,,false,31,1,941702093,"WALK",0.29964022247838484,1883404186 +4717826,1936565,"univ",1,true,1,22809,22808,193430897,"univ",,,false,25,0,1547447177,"WALK",2.48948699138067,3094894353 +4717826,1936565,"univ",1,false,4,22809,22809,193430897,"univ",10.85837416878764,22809,false,42,0,1547447181,"WALK",3.0000160707611045,3094894361 +4717826,1936565,"univ",2,false,4,22767,22809,193430897,"social",14.420134553925665,,false,43,0,1547447182,"WALK",2.7860651296874943,3094894362 +4717826,1936565,"univ",3,false,4,22767,22767,193430897,"eatout",18.783329870271647,,false,44,0,1547447183,"WALK",5.6282528531399505,3094894363 +4717826,1936565,"univ",4,false,4,22808,22767,193430897,"home",,,false,44,0,1547447184,"WALK",5.305674253948064,3094894364 +4718747,1937486,"univ",1,true,3,22771,22765,193468658,"eatout",25.835053255003054,,false,14,0,1547749265,"WALK_LOC",4.433464410699681,3095498529 +4718747,1937486,"univ",2,true,3,22767,22771,193468658,"social",25.54589732725773,,false,16,0,1547749266,"WALK",5.362458425962315,3095498530 +4718747,1937486,"univ",3,true,3,22809,22767,193468658,"univ",,,false,19,0,1547749267,"WALK",2.7945694548961417,3095498531 +4718747,1937486,"univ",1,false,1,22765,22809,193468658,"home",,,false,42,0,1547749269,"WALK",2.48457681340577,3095498537 +4718747,1937486,"shopping",1,true,2,22767,22765,193468660,"shopping",30.83670381348187,,false,13,0,1547749281,"WALK",6.4385685368034355,3095498561 +4718747,1937486,"shopping",2,true,2,22770,22767,193468660,"shopping",,,false,13,0,1547749282,"WALK",5.192455869479483,3095498562 +4718747,1937486,"shopping",1,false,1,22765,22770,193468660,"home",,,false,14,0,1547749285,"WALK",4.807792080345957,3095498569 +4720352,1939091,"univ",1,true,1,22766,22765,193534463,"univ",,,false,9,0,1548275705,"WALK",-0.6239793637995562,3096551409 +4720352,1939091,"univ",1,false,3,22759,22766,193534463,"shopping",11.15021662330538,,false,9,0,1548275709,"WALK",-0.5391508363377113,3096551417 +4720352,1939091,"univ",2,false,3,22760,22759,193534463,"othdiscr",17.448616740575964,,false,9,0,1548275710,"WALK",1.2201965059296072,3096551418 +4720352,1939091,"univ",3,false,3,22765,22760,193534463,"home",,,false,9,0,1548275711,"WALK",2.8041844809824235,3096551419 +4720352,1939091,"univ",1,true,1,22766,22765,193534464,"univ",,,false,13,0,1548275713,"WALK",2.715317834267571,3096551425 +4720352,1939091,"univ",1,false,2,22764,22766,193534464,"univ",11.320277288258279,,false,20,0,1548275717,"WALK",2.6563322165532184,3096551433 +4720352,1939091,"univ",2,false,2,22765,22764,193534464,"home",,,false,21,0,1548275718,"WALK",2.683225159417532,3096551434 +4722297,1942003,"univ",1,true,1,22809,22810,193614208,"univ",,,false,11,0,1548913665,"WALK",2.4667125356379236,3097827329 +4722297,1942003,"univ",1,false,1,22810,22809,193614208,"home",,,false,37,0,1548913669,"WALK",2.4563973988486754,3097827337 +4726458,1946164,"eatout",1,true,1,22762,22808,193784784,"eatout",,,false,21,0,1550278273,"WALK",-1.0299557646373856,3100556545 +4726458,1946164,"eatout",1,false,1,22808,22762,193784784,"home",,,false,22,0,1550278277,"WALK",-1.0299556501964702,3100556553 +4726458,1946164,"eatout",1,true,1,22773,22808,193784785,"eatout",,,false,28,0,1550278281,"WALK",-0.5777209461821046,3100556561 +4726458,1946164,"eatout",1,false,1,22808,22773,193784785,"home",,,false,29,0,1550278285,"WALK",-0.5777209461821046,3100556569 +4726458,1946164,"shopping",1,true,1,22770,22808,193784811,"shopping",,,false,14,0,1550278489,"WALK",0.3756438367025996,3100556977 +4726458,1946164,"shopping",1,false,1,22808,22770,193784811,"home",,,false,17,0,1550278493,"WALK",0.3756438367025996,3100556985 +4727363,1947069,"univ",1,true,1,22766,22765,193821914,"univ",,,false,14,0,1550575313,"WALK",-0.6239793637995562,3101150625 +4727363,1947069,"univ",1,false,3,22767,22766,193821914,"escort",13.043891235923125,,false,26,0,1550575317,"WALK",-0.9673991559282129,3101150633 +4727363,1947069,"univ",2,false,3,22767,22767,193821914,"shopping",18.14486120913688,,false,26,0,1550575318,"WALK",2.62825193059268,3101150634 +4727363,1947069,"univ",3,false,3,22765,22767,193821914,"home",,,false,27,0,1550575319,"WALK",2.1708672114306493,3101150635 +4729458,1949164,"univ",1,true,2,22767,22745,193907809,"eatout",25.639413512184284,,false,11,0,1551262473,"WALK_LOC",5.44204000187347,3102524945 +4729458,1949164,"univ",2,true,2,22809,22767,193907809,"univ",,,false,11,0,1551262474,"WALK",2.7976141090172613,3102524946 +4729458,1949164,"univ",1,false,2,22802,22809,193907809,"othdiscr",26.8381285605357,,false,27,0,1551262477,"WALK",2.9113281848126373,3102524953 +4729458,1949164,"univ",2,false,2,22745,22802,193907809,"home",,,false,28,0,1551262478,"WALK",5.509333658704657,3102524954 +4729679,1949385,"eatout",1,true,1,22748,22745,193916845,"eatout",,,false,26,0,1551334761,"WALK",0.10355029016646346,3102669521 +4729679,1949385,"eatout",1,false,1,22745,22748,193916845,"home",,,false,27,0,1551334765,"WALK",0.10355029016646346,3102669529 diff --git a/activitysim/examples/production_semcog/test/test_semcog.py b/activitysim/examples/production_semcog/test/test_semcog.py index e247fd6451..8b77a4e3a4 100644 --- a/activitysim/examples/production_semcog/test/test_semcog.py +++ b/activitysim/examples/production_semcog/test/test_semcog.py @@ -11,7 +11,7 @@ from activitysim.core.test._tools import assert_frame_substantively_equal -def run_test_semcog(multiprocess=False): +def run_test_semcog(multiprocess=False, use_explicit_error_terms=False): def example_path(dirname): resource = os.path.join("examples", "production_semcog", dirname) return str(importlib.resources.files("activitysim").joinpath(resource)) @@ -19,9 +19,12 @@ def example_path(dirname): def test_path(dirname): return os.path.join(os.path.dirname(__file__), dirname) - def regress(): + def regress(use_explicit_error_terms=False): regress_trips_df = pd.read_csv( - test_path("regress/final_trips.csv"), dtype={"depart": int} + test_path( + f"regress/final{'_eet' if use_explicit_error_terms else ''}_trips.csv" + ), + dtype={"depart": int}, ) final_trips_df = pd.read_csv( test_path("output/final_trips.csv"), dtype={"depart": int} @@ -30,6 +33,12 @@ def regress(): file_path = os.path.join(os.path.dirname(__file__), "../simulation.py") + test_config_files = [] + if use_explicit_error_terms: + test_config_files = [ + "-c", + test_path("configs_eet"), + ] if multiprocess: subprocess.run( [ @@ -37,6 +46,7 @@ def regress(): "run", "-a", file_path, + *test_config_files, "-c", test_path("configs_mp"), "-c", @@ -59,6 +69,7 @@ def regress(): "run", "-a", file_path, + *test_config_files, "-c", test_path("configs"), "-c", @@ -73,7 +84,7 @@ def regress(): check=True, ) - regress() + regress(use_explicit_error_terms=use_explicit_error_terms) def test_semcog(): @@ -84,6 +95,16 @@ def test_semcog_mp(): run_test_semcog(multiprocess=True) +def test_semcog_eet(): + run_test_semcog(multiprocess=False, use_explicit_error_terms=True) + + +def test_semcog_mp_eet(): + run_test_semcog(multiprocess=True, use_explicit_error_terms=True) + + if __name__ == "__main__": run_test_semcog(multiprocess=False) run_test_semcog(multiprocess=True) + run_test_semcog(multiprocess=False, use_explicit_error_terms=True) + run_test_semcog(multiprocess=True, use_explicit_error_terms=True) diff --git a/activitysim/examples/prototype_arc/test/configs_eet/settings.yaml b/activitysim/examples/prototype_arc/test/configs_eet/settings.yaml new file mode 100644 index 0000000000..08c06d702e --- /dev/null +++ b/activitysim/examples/prototype_arc/test/configs_eet/settings.yaml @@ -0,0 +1,3 @@ +inherit_settings: True + +use_explicit_error_terms: True diff --git a/activitysim/examples/prototype_arc/test/regress/final_trips.csv b/activitysim/examples/prototype_arc/test/regress/final_trips.csv index 3cfe9e642a..79f1c9b937 100644 --- a/activitysim/examples/prototype_arc/test/regress/final_trips.csv +++ b/activitysim/examples/prototype_arc/test/regress/final_trips.csv @@ -1,91 +1,91 @@ -trip_id,person_id,household_id,primary_purpose,trip_num,outbound,trip_count,destination,origin,tour_id,purpose,destination_logsum,depart,trip_mode,mode_choice_logsum,parking_zone_id -37314161,113762,42730,othmaint,1,True,1,106,103,4664270,othmaint,,10,DRIVEALONEFREE,-0.3567815721035004,-1 -37314165,113762,42730,othmaint,1,False,1,103,106,4664270,home,,11,DRIVEALONEFREE,-0.356460303068161,-1 -38194977,116448,43843,atwork,1,True,1,106,101,4774372,atwork,,20,DRIVEALONEFREE,-0.3217517137527465,-1 -38194981,116448,43843,atwork,1,False,1,101,106,4774372,work,,21,DRIVEALONEFREE,-0.3217517137527465,-1 -38195065,116449,43843,othdiscr,1,True,1,106,103,4774383,othdiscr,,32,SHARED2FREE,0.7593915111282218,-1 -38195069,116449,43843,othdiscr,1,False,1,103,106,4774383,home,,37,SHARED2FREE,0.7593915111282218,-1 -38195257,116448,43843,work,1,True,2,107,103,4774407,othmaint,9.244319,10,DRIVEALONEFREE,-0.6671370863914491,-1 -38195258,116448,43843,work,2,True,2,101,107,4774407,work,,10,DRIVEALONEFREE,-0.5893840193748475,-1 -38195261,116448,43843,work,1,False,1,103,101,4774407,home,,30,DRIVEALONEFREE,-0.5012716650962832,-1 -38195585,116449,43843,work,1,True,2,106,103,4774448,othmaint,10.644734,12,DRIVEALONEFREE,0.05086306230852542,-1 -38195586,116449,43843,work,2,True,2,102,106,4774448,work,,13,DRIVEALONEFREE,0.03254505218598833,-1 -38195589,116449,43843,work,1,False,3,103,102,4774448,othmaint,10.796497,23,SHARED2FREE,0.0983521099924028,-1 -38195590,116449,43843,work,2,False,3,103,103,4774448,work,12.367123,24,DRIVEALONEFREE,0.24223826711784288,-1 -38195591,116449,43843,work,3,False,3,103,103,4774448,home,,26,DRIVEALONEFREE,0.2401515927071465,-1 -38195849,116450,43843,school,1,True,1,106,103,4774481,school,,9,SCHOOL_BUS,4.351044654846191,-1 -38195853,116450,43843,school,1,False,1,103,106,4774481,home,,27,SCHOOL_BUS,4.351044654846191,-1 -38195865,116450,43843,shopping,1,True,1,101,103,4774483,shopping,,27,SHARED2FREE,-0.4441019010696936,-1 -38195869,116450,43843,shopping,1,False,1,103,101,4774483,home,,30,SHARED2FREE,-0.45749089544283433,-1 -39613905,120774,45311,atwork,1,True,1,101,102,4951738,atwork,,20,DRIVEALONEFREE,-0.41128289699554443,-1 -39613909,120774,45311,atwork,1,False,1,102,101,4951738,work,,21,DRIVEALONEFREE,-0.4119255244731903,-1 -39614185,120774,45311,work,1,True,2,106,105,4951773,work,10.647319,10,DRIVEALONEFREE,-0.4328329563140868,-1 -39614186,120774,45311,work,2,True,2,102,106,4951773,work,,11,DRIVEALONEFREE,-0.34803289175033575,-1 -39614189,120774,45311,work,1,False,1,105,102,4951773,home,,30,DRIVEALONEFREE,-0.604685664176941,-1 -39614513,120775,45311,work,1,True,1,101,105,4951814,work,,9,DRIVEALONEFREE,-0.6009435653686525,-1 -39614517,120775,45311,work,1,False,3,101,101,4951814,work,10.767546,28,DRIVEALONEFREE,-0.3567099869251252,-1 -39614518,120775,45311,work,2,False,3,107,101,4951814,othmaint,9.370711,28,DRIVEALONEFREE,-0.5956825017929079,-1 -39614519,120775,45311,work,3,False,3,105,107,4951814,home,,29,DRIVEALONEFREE,-0.43356654047966,-1 -40387937,123133,46056,work,1,True,1,106,106,5048492,work,,20,DRIVEALONEFREE,-0.19777289032936102,-1 -40387941,123133,46056,work,1,False,1,106,106,5048492,home,,40,DRIVEALONEFREE,-0.1974023878574371,-1 -43308361,132037,49258,othmaint,1,True,1,122,110,5413545,othmaint,,23,DRIVEALONEFREE,-0.7390050888061525,-1 -43308365,132037,49258,othmaint,1,False,2,114,122,5413545,eatout,8.7858,24,DRIVEALONEFREE,-0.5175821781158448,-1 -43308366,132037,49258,othmaint,2,False,2,110,114,5413545,home,,24,DRIVEALONEFREE,-0.5938398838043213,-1 -43308537,132038,49258,escort,1,True,1,107,110,5413567,escort,,10,SHARED3FREE,-0.002601420005322437,-1 -43308541,132038,49258,escort,1,False,1,110,107,5413567,home,,22,SHARED3FREE,-0.002601420005322437,-1 -44930737,136983,50912,work,1,True,2,123,112,5616342,eatout,9.353397,31,DRIVEALONEFREE,-0.5493329763412477,-1 -44930738,136983,50912,work,2,True,2,104,123,5616342,work,,32,DRIVEALONEFREE,-0.6666110157966614,-1 -44930741,136983,50912,work,1,False,2,112,104,5616342,social,11.149774,34,DRIVEALONEFREE,-0.5302670001983643,-1 -44930742,136983,50912,work,2,False,2,112,112,5616342,home,,34,DRIVEALONEFREE,-0.18331599235534674,-1 -44931065,136984,50912,work,1,True,2,101,112,5616383,shopping,9.520916,11,DRIVEALONEFREE,-0.6129478216171266,-1 -44931066,136984,50912,work,2,True,2,107,101,5616383,work,,12,DRIVEALONEFREE,-0.6193944811820985,-1 -44931069,136984,50912,work,1,False,3,123,107,5616383,work,10.775923,28,DRIVEALONEFREE,-0.7651270031929017,-1 -44931070,136984,50912,work,2,False,3,104,123,5616383,escort,9.519634,29,DRIVEALONEFREE,-0.6666110157966614,-1 -44931071,136984,50912,work,3,False,3,112,104,5616383,home,,30,DRIVEALONEFREE,-0.5499035120010376,-1 -47621473,145187,53716,othmaint,1,True,3,121,116,5952684,social,9.947862,8,SHARED3FREE,-0.41955729937135083,-1 -47621474,145187,53716,othmaint,2,True,3,112,121,5952684,othmaint,9.261029,11,SHARED3FREE,-0.6422730088233947,-1 -47621475,145187,53716,othmaint,3,True,3,122,112,5952684,othmaint,,11,SHARED3FREE,-0.6419082880020143,-1 -47621477,145187,53716,othmaint,1,False,1,116,122,5952684,home,,20,SHARED3FREE,-0.6134629858242939,-1 -47621737,145188,53716,escort,1,True,1,114,116,5952717,escort,,29,DRIVEALONEFREE,-0.15083796859645277,-1 -47621741,145188,53716,escort,1,False,1,116,114,5952717,home,,30,SHARED2FREE,-0.15179812895272474,-1 -47622241,145189,53716,school,1,True,1,114,116,5952780,school,,10,SCHOOL_BUS,4.3079237937927255,-1 -47622245,145189,53716,school,1,False,1,116,114,5952780,home,,24,SCHOOL_BUS,4.3079237937927255,-1 -47622569,145190,53716,school,1,True,1,114,116,5952821,school,,9,SHARED2FREE,-0.20617904275545365,-1 -47622573,145190,53716,school,1,False,1,116,114,5952821,home,,24,SHARED2FREE,-0.20568500108204935,-1 -48258513,147129,54342,othdiscr,1,True,1,116,117,6032314,othdiscr,,27,DRIVEALONEFREE,-0.5246167778968812,-1 -48258517,147129,54342,othdiscr,1,False,1,117,116,6032314,home,,33,DRIVEALONEFREE,-0.49120157957077026,-1 -48258537,147129,54342,othmaint,1,True,1,114,117,6032317,othmaint,,34,DRIVEALONEFREE,-0.687132179737091,-1 -48258541,147129,54342,othmaint,1,False,2,114,114,6032317,shopping,9.148774,37,DRIVEALONEFREE,-0.42373609542846685,-1 -48258542,147129,54342,othmaint,2,False,2,117,114,6032317,home,,38,DRIVEALONEFREE,-0.6845617890357972,-1 -56357665,171822,63802,eatout,1,True,1,127,135,7044708,eatout,,31,DRIVEALONEFREE,-0.6526245474815369,-1 -56357669,171822,63802,eatout,1,False,1,135,127,7044708,home,,34,DRIVEALONEFREE,-0.6343104243278503,-1 -56357689,171822,63802,escort,1,True,1,135,135,7044711,escort,,28,SHARED3FREE,0.07706324286670248,-1 -56357693,171822,63802,escort,1,False,2,135,135,7044711,escort,11.356267,28,SHARED3FREE,0.07706324286670248,-1 -56357694,171822,63802,escort,2,False,2,135,135,7044711,home,,28,SHARED3FREE,0.07706324286670248,-1 -56357737,171822,63802,othdiscr,1,True,3,131,135,7044717,othdiscr,12.194779,13,SHARED2FREE,0.599977654783949,-1 -56357738,171822,63802,othdiscr,2,True,3,130,131,7044717,shopping,13.357507,14,SHARED2FREE,0.6200047250329787,-1 -56357739,171822,63802,othdiscr,3,True,3,130,130,7044717,othdiscr,,14,SHARED2FREE,0.6960546579187884,-1 -56357741,171822,63802,othdiscr,1,False,1,135,130,7044717,home,,14,SHARED2FREE,0.6487159186367744,-1 -56358209,171823,63802,shopping,1,True,4,131,135,7044776,othmaint,10.342613,24,SHARED3FREE,-0.14619837454037923,-1 -56358210,171823,63802,shopping,2,True,4,131,131,7044776,social,12.281772,25,SHARED3FREE,-0.012169709209450414,-1 -56358211,171823,63802,shopping,3,True,4,131,131,7044776,shopping,11.556939,26,SHARED3FREE,-0.012169709209450414,-1 -56358212,171823,63802,shopping,4,True,4,131,131,7044776,shopping,,26,SHARED3FREE,-0.012169709209450414,-1 -56358213,171823,63802,shopping,1,False,1,135,131,7044776,home,,27,DRIVEALONEFREE,-0.15095594351539895,-1 -56358473,171824,63802,othdiscr,1,True,1,131,135,7044809,othdiscr,,32,SHARED2FREE,-0.46024149381952484,-1 -56358477,171824,63802,othdiscr,1,False,1,135,131,7044809,home,,37,SHARED2FREE,-0.45329299190068956,-1 -56358521,171824,63802,school,1,True,2,135,135,7044815,escort,11.635028,10,SHARED2FREE,0.10569338088788001,-1 -56358522,171824,63802,school,2,True,2,135,135,7044815,school,,10,SHARED3FREE,0.10569338088788001,-1 -56358525,171824,63802,school,1,False,2,135,135,7044815,othdiscr,11.906311,25,SHARED3FREE,0.10545807803885715,-1 -56358526,171824,63802,school,2,False,2,135,135,7044815,home,,26,SHARED3FREE,0.10545807803885715,-1 -56358801,171825,63802,othdiscr,1,True,1,131,135,7044850,othdiscr,,29,SHARED3FREE,-0.281769477499857,-1 -56358805,171825,63802,othdiscr,1,False,2,132,131,7044850,social,10.225653,35,SHARED2FREE,-0.20277185632585107,-1 -56358806,171825,63802,othdiscr,2,False,2,135,132,7044850,home,,39,SHARED3FREE,-0.36521793162300004,-1 -56358809,171825,63802,othdiscr,1,True,4,135,135,7044851,othmaint,5.3795877,26,WALK,-0.7460585832595825,-1 -56358810,171825,63802,othdiscr,2,True,4,131,135,7044851,othmaint,5.4266872,27,WALK,-2.0398435592651363,-1 -56358811,171825,63802,othdiscr,3,True,4,130,131,7044851,othmaint,5.7105064,28,WALK,-1.2828608751297,-1 -56358812,171825,63802,othdiscr,4,True,4,130,130,7044851,othdiscr,,28,WALK,-0.78075897693634,-1 -56358813,171825,63802,othdiscr,1,False,1,135,130,7044851,home,,28,WALK,-1.4660019874572756,-1 -56358849,171825,63802,school,1,True,1,135,135,7044856,school,,9,SHARED3FREE,0.10569338088788001,-1 -56358853,171825,63802,school,1,False,1,135,135,7044856,home,,24,SHARED3FREE,0.10569338088788001,-1 -56359177,171826,63802,school,1,True,1,135,135,7044897,school,,10,SHARED3FREE,0.10569338088788001,-1 -56359181,171826,63802,school,1,False,1,135,135,7044897,home,,22,SHARED3FREE,0.10569338088788001,-1 +"person_id","household_id","primary_purpose","trip_num","outbound","trip_count","destination","origin","tour_id","purpose","destination_logsum","depart","trip_mode","mode_choice_logsum","trip_id" +113762,42730,"othmaint",1,true,1,106,103,4664270,"othmaint",,10,"DRIVEALONEFREE",-0.3567815833091734,37314161 +113762,42730,"othmaint",1,false,1,103,106,4664270,"home",,11,"DRIVEALONEFREE",-0.3564603142738344,37314165 +116448,43843,"atwork",1,true,1,106,101,4774372,"atwork",,20,"DRIVEALONEFREE",-0.3217517094135284,38194977 +116448,43843,"atwork",1,false,1,101,106,4774372,"work",,21,"DRIVEALONEFREE",-0.3217517094135284,38194981 +116449,43843,"othdiscr",1,true,1,106,103,4774383,"othdiscr",,32,"SHARED2FREE",0.7593914979829192,38195065 +116449,43843,"othdiscr",1,false,1,103,106,4774383,"home",,37,"SHARED2FREE",0.7593914979829192,38195069 +116448,43843,"work",1,true,2,107,103,4774407,"othmaint",9.244319214996622,10,"DRIVEALONEFREE",-0.6671371741294861,38195257 +116448,43843,"work",2,true,2,101,107,4774407,"work",,11,"DRIVEALONEFREE",-0.5893840121269226,38195258 +116448,43843,"work",1,false,1,103,101,4774407,"home",,30,"DRIVEALONEFREE",-0.5012717045307159,38195261 +116449,43843,"work",1,true,2,106,103,4774448,"othmaint",10.644734946815246,12,"DRIVEALONEFREE",0.05086305622830629,38195585 +116449,43843,"work",2,true,2,102,106,4774448,"work",,15,"DRIVEALONEFREE",0.03291252148410589,38195586 +116449,43843,"work",1,false,3,103,102,4774448,"othmaint",10.796498240479236,24,"SHARED2FREE",0.09835208854623434,38195589 +116449,43843,"work",2,false,3,103,103,4774448,"work",12.367122837815295,26,"DRIVEALONEFREE",0.24015159118542195,38195590 +116449,43843,"work",3,false,3,103,103,4774448,"home",,26,"DRIVEALONEFREE",0.24015159118542195,38195591 +116450,43843,"school",1,true,1,106,103,4774481,"school",,9,"SCHOOL_BUS",4.351044797545671,38195849 +116450,43843,"school",1,false,1,103,106,4774481,"home",,27,"SCHOOL_BUS",4.351044797545671,38195853 +116450,43843,"shopping",1,true,1,101,103,4774483,"shopping",,27,"SHARED2FREE",-0.44410188801130307,38195865 +116450,43843,"shopping",1,false,1,103,101,4774483,"home",,30,"SHARED2FREE",-0.4574908823858229,38195869 +120774,45311,"atwork",1,true,1,101,102,4951738,"atwork",,20,"DRIVEALONEFREE",-0.4112828999996184,39613905 +120774,45311,"atwork",1,false,1,102,101,4951738,"work",,21,"DRIVEALONEFREE",-0.4119254976749421,39613909 +120774,45311,"work",1,true,2,106,105,4951773,"work",10.647318549180723,10,"DRIVEALONEFREE",-0.4328329442501069,39614185 +120774,45311,"work",2,true,2,102,106,4951773,"work",,11,"DRIVEALONEFREE",-0.3480328878879547,39614186 +120774,45311,"work",1,false,1,105,102,4951773,"home",,30,"DRIVEALONEFREE",-0.6046856504917144,39614189 +120775,45311,"work",1,true,1,101,105,4951814,"work",,9,"DRIVEALONEFREE",-0.6009435992240908,39614513 +120775,45311,"work",1,false,3,101,101,4951814,"work",10.767545755383658,26,"DRIVEALONEFREE",-0.35670998854637154,39614517 +120775,45311,"work",2,false,3,107,101,4951814,"othmaint",9.370711100307654,26,"DRIVEALONEFREE",-0.5956824945449828,39614518 +120775,45311,"work",3,false,3,105,107,4951814,"home",,29,"DRIVEALONEFREE",-0.43356653208732604,39614519 +123133,46056,"work",1,true,1,106,106,5048492,"work",,20,"DRIVEALONEFREE",-0.19777291302680963,40387937 +123133,46056,"work",1,false,1,106,106,5048492,"home",,40,"DRIVEALONEFREE",-0.1974023956537246,40387941 +132037,49258,"othmaint",1,true,1,122,110,5413545,"othmaint",,23,"DRIVEALONEFREE",-0.7390051318168641,43308361 +132037,49258,"othmaint",1,false,2,114,122,5413545,"eatout",8.785799297132586,24,"DRIVEALONEFREE",-0.5175821724891663,43308365 +132037,49258,"othmaint",2,false,2,110,114,5413545,"home",,24,"DRIVEALONEFREE",-0.5938398692131043,43308366 +132038,49258,"escort",1,true,1,107,110,5413567,"escort",,10,"SHARED3FREE",-0.002601425939803153,43308537 +132038,49258,"escort",1,false,1,110,107,5413567,"home",,22,"SHARED3FREE",-0.002601425939803153,43308541 +136983,50912,"work",1,true,2,123,112,5616342,"eatout",9.353397383302754,31,"DRIVEALONEFREE",-0.5493329919815063,44930737 +136983,50912,"work",2,true,2,104,123,5616342,"work",,32,"DRIVEALONEFREE",-0.6666110144615174,44930738 +136983,50912,"work",1,false,2,112,104,5616342,"social",11.149774183428809,32,"DRIVEALONEFREE",-0.5499035404682159,44930741 +136983,50912,"work",2,false,2,112,112,5616342,"home",,34,"DRIVEALONEFREE",-0.18331599397659298,44930742 +136984,50912,"work",1,true,2,101,112,5616383,"shopping",9.520915574738705,11,"DRIVEALONEFREE",-0.6129478299617769,44931065 +136984,50912,"work",2,true,2,107,101,5616383,"work",,11,"DRIVEALONEFREE",-0.6193944739341735,44931066 +136984,50912,"work",1,false,3,123,107,5616383,"work",10.775923228059439,27,"DRIVEALONEFREE",-0.7651270068168641,44931069 +136984,50912,"work",2,false,3,104,123,5616383,"escort",9.51963410471578,27,"DRIVEALONEFREE",-0.6666110144615174,44931070 +136984,50912,"work",3,false,3,112,104,5616383,"home",,30,"DRIVEALONEFREE",-0.5499035404682159,44931071 +145187,53716,"othmaint",1,true,3,121,116,5952684,"social",9.947861733897312,8,"SHARED3FREE",-0.41955731333905055,47621473 +145187,53716,"othmaint",2,true,3,112,121,5952684,"othmaint",9.26102871194454,11,"SHARED3FREE",-0.6422730395793916,47621474 +145187,53716,"othmaint",3,true,3,122,112,5952684,"othmaint",,11,"SHARED3FREE",-0.6419082540988923,47621475 +145187,53716,"othmaint",1,false,1,116,122,5952684,"home",,20,"SHARED3FREE",-0.6134629528820534,47621477 +145188,53716,"escort",1,true,1,114,116,5952717,"escort",,29,"DRIVEALONEFREE",-0.15083797590032313,47621737 +145188,53716,"escort",1,false,1,116,114,5952717,"home",,30,"SHARED2FREE",-0.15179813514136692,47621741 +145189,53716,"school",1,true,1,114,116,5952780,"school",,10,"SCHOOL_BUS",4.3079239998221395,47622241 +145189,53716,"school",1,false,1,116,114,5952780,"home",,24,"SCHOOL_BUS",4.3079239998221395,47622245 +145190,53716,"school",1,true,1,114,116,5952821,"school",,9,"SHARED2FREE",-0.20617904920050897,47622569 +145190,53716,"school",1,false,1,116,114,5952821,"home",,24,"SHARED2FREE",-0.20568500752544042,47622573 +147129,54342,"othdiscr",1,true,1,116,117,6032314,"othdiscr",,27,"DRIVEALONEFREE",-0.5246167486667632,48258513 +147129,54342,"othdiscr",1,false,1,117,116,6032314,"home",,33,"DRIVEALONEFREE",-0.4912015503406525,48258517 +147129,54342,"othmaint",1,true,1,114,117,6032317,"othmaint",,34,"DRIVEALONEFREE",-0.6871321834564209,48258537 +147129,54342,"othmaint",1,false,2,114,114,6032317,"shopping",9.148774093624228,37,"DRIVEALONEFREE",-0.4237361037254333,48258541 +147129,54342,"othmaint",2,false,2,117,114,6032317,"home",,38,"DRIVEALONEFREE",-0.6845617927551271,48258542 +171822,63802,"eatout",1,true,1,127,135,7044708,"eatout",,31,"DRIVEALONEFREE",-0.652624578666687,56357665 +171822,63802,"eatout",1,false,1,135,127,7044708,"home",,34,"DRIVEALONEFREE",-0.6343104555130004,56357669 +171822,63802,"escort",1,true,1,135,135,7044711,"escort",,28,"SHARED3FREE",0.07706324792840326,56357689 +171822,63802,"escort",1,false,2,135,135,7044711,"escort",11.356267091092906,28,"SHARED3FREE",0.07706324792840326,56357693 +171822,63802,"escort",2,false,2,135,135,7044711,"home",,28,"SHARED3FREE",0.07706324792840326,56357694 +171822,63802,"othdiscr",1,true,3,131,135,7044717,"othdiscr",12.194779637866755,13,"SHARED2FREE",0.5999776535886836,56357737 +171822,63802,"othdiscr",2,true,3,130,131,7044717,"shopping",13.357506128369907,13,"SHARED2FREE",0.620004705610611,56357738 +171822,63802,"othdiscr",3,true,3,130,130,7044717,"othdiscr",,13,"SHARED2FREE",0.6960546331136191,56357739 +171822,63802,"othdiscr",1,false,1,135,130,7044717,"home",,14,"SHARED2FREE",0.6487159219305315,56357741 +171823,63802,"shopping",1,true,4,131,135,7044776,"othmaint",10.342612763246748,24,"SHARED3FREE",-0.1461983375796215,56358209 +171823,63802,"shopping",2,true,4,131,131,7044776,"social",12.281771137855209,25,"SHARED3FREE",-0.01216970989402637,56358210 +171823,63802,"shopping",3,true,4,131,131,7044776,"shopping",11.556938954932807,26,"SHARED3FREE",-0.01216970989402637,56358211 +171823,63802,"shopping",4,true,4,131,131,7044776,"shopping",,26,"SHARED3FREE",-0.01216970989402637,56358212 +171823,63802,"shopping",1,false,1,135,131,7044776,"home",,27,"DRIVEALONEFREE",-0.1509559426724583,56358213 +171824,63802,"othdiscr",1,true,1,131,135,7044809,"othdiscr",,32,"SHARED2FREE",-0.4602414968534174,56358473 +171824,63802,"othdiscr",1,false,1,135,131,7044809,"home",,37,"SHARED2FREE",-0.45329299490631025,56358477 +171824,63802,"school",1,true,2,135,135,7044815,"escort",11.63502708221396,10,"SHARED2FREE",0.10569338295856193,56358521 +171824,63802,"school",2,true,2,135,135,7044815,"school",,11,"SHARED3FREE",0.10569338295856193,56358522 +171824,63802,"school",1,false,2,135,135,7044815,"othdiscr",11.906310169709501,25,"SHARED3FREE",0.10545807870230886,56358525 +171824,63802,"school",2,false,2,135,135,7044815,"home",,26,"SHARED3FREE",0.10545807870230886,56358526 +171825,63802,"othdiscr",1,true,1,131,135,7044850,"othdiscr",,29,"SHARED3FREE",-0.2817694611797112,56358801 +171825,63802,"othdiscr",1,false,2,132,131,7044850,"social",10.225652936631004,39,"SHARED2FREE",-0.20277182093145324,56358805 +171825,63802,"othdiscr",2,false,2,135,132,7044850,"home",,39,"SHARED3FREE",-0.36521793543833225,56358806 +171825,63802,"othdiscr",1,true,4,135,135,7044851,"othmaint",5.3795880164987455,26,"WALK",-0.7460586428642273,56358809 +171825,63802,"othdiscr",2,true,4,131,135,7044851,"othmaint",5.426687572911977,27,"WALK",-2.039843797683716,56358810 +171825,63802,"othdiscr",3,true,4,130,131,7044851,"othmaint",5.710506288696134,28,"WALK",-1.2828608453273775,56358811 +171825,63802,"othdiscr",4,true,4,130,130,7044851,"othdiscr",,28,"WALK",-0.7807589769363404,56358812 +171825,63802,"othdiscr",1,false,1,135,130,7044851,"home",,28,"WALK",-1.4660018980503084,56358813 +171825,63802,"school",1,true,1,135,135,7044856,"school",,9,"SHARED3FREE",0.10569338295856193,56358849 +171825,63802,"school",1,false,1,135,135,7044856,"home",,24,"SHARED3FREE",0.10569338295856193,56358853 +171826,63802,"school",1,true,1,135,135,7044897,"school",,10,"SHARED3FREE",0.10569338295856193,56359177 +171826,63802,"school",1,false,1,135,135,7044897,"home",,22,"SHARED3FREE",0.10569338295856193,56359181 diff --git a/activitysim/examples/prototype_arc/test/regress/final_trips_eet.csv b/activitysim/examples/prototype_arc/test/regress/final_trips_eet.csv new file mode 100644 index 0000000000..ca0a19fb4c --- /dev/null +++ b/activitysim/examples/prototype_arc/test/regress/final_trips_eet.csv @@ -0,0 +1,84 @@ +"person_id","household_id","primary_purpose","trip_num","outbound","trip_count","destination","origin","tour_id","purpose","destination_logsum","depart","trip_mode","mode_choice_logsum","trip_id" +113762,42730,"work",1,true,2,102,103,4664281,"escort",11.620562271430003,11,"SHARED2FREE",0.1984450162064936,37314249 +113762,42730,"work",2,true,2,106,102,4664281,"work",,12,"DRIVEALONEFREE",0.1174194651741796,37314250 +113762,42730,"work",1,false,2,101,106,4664281,"shopping",11.71157893264538,31,"SHARED3FREE",0.16362355684245977,37314253 +113762,42730,"work",2,false,2,103,101,4664281,"home",,32,"DRIVEALONEFREE",-0.02897760267828356,37314254 +116448,43843,"work",1,true,2,105,103,4774407,"othmaint",9.341245249202924,10,"DRIVEALONEFREE",-0.657585671377182,38195257 +116448,43843,"work",2,true,2,101,105,4774407,"work",,15,"DRIVEALONEFREE",-0.5883466045856476,38195258 +116448,43843,"work",1,false,2,123,101,4774407,"othmaint",9.123561456324088,25,"DRIVEALONEFREE",-0.9421370327949524,38195261 +116448,43843,"work",2,false,2,103,123,4774407,"home",,29,"DRIVEALONEFREE",-0.7305148571968079,38195262 +116450,43843,"school",1,true,2,106,103,4774481,"escort",11.70931236960696,10,"SHARED3FREE",0.06412222350024642,38195849 +116450,43843,"school",2,true,2,106,106,4774481,"school",,10,"WALK",0.1346124978975689,38195850 +116450,43843,"school",1,false,1,103,106,4774481,"home",,26,"SHARED3FREE",0.06400167836417262,38195853 +120774,45311,"atwork",1,true,2,122,123,4951738,"work",11.450943959940382,18,"DRIVEALONEFREE",-0.481071140050888,39613905 +120774,45311,"atwork",2,true,2,122,122,4951738,"atwork",,18,"DRIVEALONEFREE",-0.3282438698291779,39613906 +120774,45311,"atwork",1,false,1,123,122,4951738,"work",,21,"DRIVEALONEFREE",-0.47753682303428663,39613909 +120774,45311,"work",1,true,1,123,105,4951773,"work",,8,"DRIVEALONEFREE",-0.5730820206642151,39614185 +120774,45311,"work",1,false,1,105,123,4951773,"home",,29,"DRIVEALONEFREE",-0.5845674780845644,39614189 +120775,45311,"shopping",1,true,1,104,105,4951808,"shopping",,19,"DRIVEALONEFREE",-0.5170302743911744,39614465 +120775,45311,"shopping",1,false,1,105,104,4951808,"home",,26,"DRIVEALONEFREE",-0.526026701450348,39614469 +120775,45311,"work",1,true,1,106,105,4951814,"work",,11,"DRIVEALONEFREE",-0.4328329442501069,39614513 +120775,45311,"work",1,false,1,105,106,4951814,"home",,19,"DRIVEALONEFREE",-0.4224589346408844,39614517 +123132,46056,"atwork",1,true,2,106,106,5048416,"othdiscr",5.736908684681538,20,"WALK",-1.0410120487213135,40387329 +123132,46056,"atwork",2,true,2,101,106,5048416,"atwork",,21,"WALK",-1.4049548953771591,40387330 +123132,46056,"atwork",1,false,1,106,101,5048416,"work",,22,"WALK",-1.4049548953771591,40387333 +123132,46056,"work",1,true,2,100,106,5048451,"social",12.992372530395093,11,"DRIVEALONEFREE",-0.056856693416576155,40387609 +123132,46056,"work",2,true,2,106,100,5048451,"work",,11,"SHARED2FREE",-0.06978704502587849,40387610 +123132,46056,"work",1,false,4,106,106,5048451,"othmaint",11.571615647229482,30,"DRIVEALONEFREE",0.2365447098181202,40387613 +123132,46056,"work",2,false,4,106,106,5048451,"othmaint",12.148331580258795,30,"SHARED2FREE",0.2365447098181202,40387614 +123132,46056,"work",3,false,4,106,106,5048451,"eatout",11.338764994103533,30,"DRIVEALONEFREE",0.2365447098181202,40387615 +123132,46056,"work",4,false,4,106,106,5048451,"home",,35,"SHARED2FREE",0.23932365335602357,40387616 +136983,50912,"atwork",1,true,1,123,123,5616307,"atwork",,19,"WALK",-1.127763032913208,44930457 +136983,50912,"atwork",1,false,2,123,123,5616307,"othmaint",6.824969769883364,19,"WALK",-1.127763032913208,44930461 +136983,50912,"atwork",2,false,2,123,123,5616307,"work",,21,"WALK",-1.127763032913208,44930462 +136983,50912,"work",1,true,1,123,112,5616342,"work",,16,"SHARED3FREE",-0.020760706766682024,44930737 +136983,50912,"work",1,false,4,104,123,5616342,"eatout",11.284613956027043,23,"SHARED3FREE",-0.07389233569315362,44930741 +136983,50912,"work",2,false,4,103,104,5616342,"work",12.6148435779502,23,"SHARED3FREE",0.05220751420566755,44930742 +136983,50912,"work",3,false,4,104,103,5616342,"eatout",10.989757771316299,23,"DRIVEALONEFREE",0.053315758831313835,44930743 +136983,50912,"work",4,false,4,112,104,5616342,"home",,30,"DRIVEALONEFREE",-0.03395123851246563,44930744 +136984,50912,"atwork",1,true,1,111,114,5616344,"atwork",,19,"DRIVEALONEFREE",-0.5068167327404024,44930753 +136984,50912,"atwork",1,false,1,114,111,5616344,"work",,20,"DRIVEALONEFREE",-0.50456764087677,44930757 +136984,50912,"atwork",1,true,1,114,114,5616348,"atwork",,20,"DRIVEALONEFREE",-0.4243787014007569,44930785 +136984,50912,"atwork",1,false,1,114,114,5616348,"work",,21,"DRIVEALONEFREE",-0.4243787014007569,44930789 +136984,50912,"work",1,true,1,114,112,5616383,"work",,17,"DRIVEALONEFREE",-0.5196855528831482,44931065 +136984,50912,"work",1,false,1,112,114,5616383,"home",,32,"DRIVEALONEFREE",-0.524872527885437,44931069 +145188,53716,"othdiscr",1,true,2,103,116,5952733,"eatout",8.095380549568242,26,"SHARED2FREE",-0.656344788392591,47621865 +145188,53716,"othdiscr",2,true,2,106,103,5952733,"othdiscr",,28,"SHARED3FREE",0.01085078798134025,47621866 +145188,53716,"othdiscr",1,false,1,116,106,5952733,"home",,39,"SHARED3FREE",-0.5586695841155525,47621869 +145188,53716,"shopping",1,true,2,121,116,5952741,"escort",10.284349421053356,24,"DRIVEALONEFREE",0.007844892145818317,47621929 +145188,53716,"shopping",2,true,2,122,121,5952741,"shopping",,24,"DRIVEALONEFREE",-0.09430664622810597,47621930 +145188,53716,"shopping",1,false,1,116,122,5952741,"home",,25,"DRIVEALONEFREE",-0.18333346470683046,47621933 +147129,54342,"atwork",1,true,2,118,118,6032293,"work",11.591261619486666,24,"DRIVEALONEFREE",-0.277168506860733,48258345 +147129,54342,"atwork",2,true,2,118,118,6032293,"atwork",,25,"DRIVEALONEFREE",-0.27748977589607243,48258346 +147129,54342,"atwork",1,false,1,118,118,6032293,"work",,27,"DRIVEALONEFREE",-0.27748977589607243,48258349 +147129,54342,"work",1,true,1,118,117,6032328,"work",,24,"DRIVEALONEFREE",-0.3512043696403504,48258625 +147129,54342,"work",1,false,2,117,118,6032328,"othmaint",9.222307568325903,45,"DRIVEALONEFREE",-0.3512043696403504,48258629 +147129,54342,"work",2,false,2,117,117,6032328,"home",,46,"DRIVEALONEFREE",-0.3278332107067109,48258630 +168909,62701,"othmaint",1,true,1,135,131,6925297,"othmaint",,25,"DRIVEALONEFREE",-0.1509559426724583,55402377 +168909,62701,"othmaint",1,false,1,131,135,6925297,"home",,28,"SHARED3FREE",-0.14873576359938484,55402381 +171822,63802,"atwork",1,true,1,128,130,7044702,"atwork",,17,"DRIVEALONEFREE",-0.5419102621078492,56357617 +171822,63802,"atwork",1,false,1,130,128,7044702,"work",,20,"DRIVEALONEFREE",-0.5419102621078492,56357621 +171822,63802,"shopping",1,true,1,135,135,7044721,"shopping",,29,"WALK",-0.7460586428642273,56357769 +171822,63802,"shopping",1,false,3,135,135,7044721,"othmaint",8.365401255841729,30,"WALK",-0.7460586428642273,56357773 +171822,63802,"shopping",2,false,3,135,135,7044721,"othmaint",8.36277534519151,30,"WALK",-0.7460586428642273,56357774 +171822,63802,"shopping",3,false,3,135,135,7044721,"home",,31,"WALK",-0.7460586428642273,56357775 +171822,63802,"work",1,true,1,130,135,7044741,"work",,12,"DRIVEALONEFREE",-0.4857847907543183,56357929 +171822,63802,"work",1,false,2,130,130,7044741,"shopping",9.965446400011412,24,"DRIVEALONEFREE",-0.45575206193923956,56357933 +171822,63802,"work",2,false,2,135,130,7044741,"home",,27,"DRIVEALONEFREE",-0.486525795698166,56357934 +171823,63802,"escort",1,true,1,135,135,7044752,"escort",,27,"SHARED2FREE",0.07706324792840326,56358017 +171823,63802,"escort",1,false,2,135,135,7044752,"othmaint",11.334171843971035,27,"DRIVEALONEFREE",0.07706324792840326,56358021 +171823,63802,"escort",2,false,2,135,135,7044752,"home",,27,"SHARED3FREE",0.07706324792840326,56358022 +171823,63802,"escort",1,true,1,135,135,7044753,"escort",,10,"SHARED2FREE",0.07768184479155238,56358025 +171823,63802,"escort",1,false,3,135,135,7044753,"escort",11.396653773741745,10,"SHARED3FREE",0.07768184479155238,56358029 +171823,63802,"escort",2,false,3,135,135,7044753,"eatout",11.163952601101887,10,"WALK",0.07768184479155238,56358030 +171823,63802,"escort",3,false,3,135,135,7044753,"home",,11,"SHARED2FREE",0.07768184479155238,56358031 +171824,63802,"school",1,true,1,135,135,7044815,"school",,10,"SHARED2FREE",-0.04192782886512184,56358521 +171824,63802,"school",1,false,1,135,135,7044815,"home",,25,"SHARED2FREE",-0.0421655910777971,56358525 +171825,63802,"school",1,true,2,135,135,7044856,"school",14.150222701978416,10,"SHARED3FREE",0.10569338295856193,56358849 +171825,63802,"school",2,true,2,135,135,7044856,"school",,11,"SHARED3FREE",0.10569338295856193,56358850 +171825,63802,"school",1,false,1,135,135,7044856,"home",,26,"SHARED3FREE",0.10545807870230886,56358853 +171826,63802,"school",1,true,3,135,135,7044897,"othmaint",11.076463602650021,8,"SHARED2FREE",-0.04192782886512184,56359177 +171826,63802,"school",2,true,3,135,135,7044897,"escort",11.412423645394568,9,"SHARED2FREE",-0.04192782886512184,56359178 +171826,63802,"school",3,true,3,135,135,7044897,"school",,9,"SHARED2FREE",-0.04192782886512184,56359179 +171826,63802,"school",1,false,2,135,135,7044897,"school",13.560897172707032,22,"SHARED2FREE",-0.04192782886512184,56359181 +171826,63802,"school",2,false,2,135,135,7044897,"home",,24,"SHARED2FREE",-0.04192782886512184,56359182 diff --git a/activitysim/examples/prototype_arc/test/regress/final_trips_sh.csv b/activitysim/examples/prototype_arc/test/regress/final_trips_sh.csv index 3cfe9e642a..0bd93ac3e8 100644 --- a/activitysim/examples/prototype_arc/test/regress/final_trips_sh.csv +++ b/activitysim/examples/prototype_arc/test/regress/final_trips_sh.csv @@ -1,91 +1,91 @@ -trip_id,person_id,household_id,primary_purpose,trip_num,outbound,trip_count,destination,origin,tour_id,purpose,destination_logsum,depart,trip_mode,mode_choice_logsum,parking_zone_id -37314161,113762,42730,othmaint,1,True,1,106,103,4664270,othmaint,,10,DRIVEALONEFREE,-0.3567815721035004,-1 -37314165,113762,42730,othmaint,1,False,1,103,106,4664270,home,,11,DRIVEALONEFREE,-0.356460303068161,-1 -38194977,116448,43843,atwork,1,True,1,106,101,4774372,atwork,,20,DRIVEALONEFREE,-0.3217517137527465,-1 -38194981,116448,43843,atwork,1,False,1,101,106,4774372,work,,21,DRIVEALONEFREE,-0.3217517137527465,-1 -38195065,116449,43843,othdiscr,1,True,1,106,103,4774383,othdiscr,,32,SHARED2FREE,0.7593915111282218,-1 -38195069,116449,43843,othdiscr,1,False,1,103,106,4774383,home,,37,SHARED2FREE,0.7593915111282218,-1 -38195257,116448,43843,work,1,True,2,107,103,4774407,othmaint,9.244319,10,DRIVEALONEFREE,-0.6671370863914491,-1 -38195258,116448,43843,work,2,True,2,101,107,4774407,work,,10,DRIVEALONEFREE,-0.5893840193748475,-1 -38195261,116448,43843,work,1,False,1,103,101,4774407,home,,30,DRIVEALONEFREE,-0.5012716650962832,-1 -38195585,116449,43843,work,1,True,2,106,103,4774448,othmaint,10.644734,12,DRIVEALONEFREE,0.05086306230852542,-1 -38195586,116449,43843,work,2,True,2,102,106,4774448,work,,13,DRIVEALONEFREE,0.03254505218598833,-1 -38195589,116449,43843,work,1,False,3,103,102,4774448,othmaint,10.796497,23,SHARED2FREE,0.0983521099924028,-1 -38195590,116449,43843,work,2,False,3,103,103,4774448,work,12.367123,24,DRIVEALONEFREE,0.24223826711784288,-1 -38195591,116449,43843,work,3,False,3,103,103,4774448,home,,26,DRIVEALONEFREE,0.2401515927071465,-1 -38195849,116450,43843,school,1,True,1,106,103,4774481,school,,9,SCHOOL_BUS,4.351044654846191,-1 -38195853,116450,43843,school,1,False,1,103,106,4774481,home,,27,SCHOOL_BUS,4.351044654846191,-1 -38195865,116450,43843,shopping,1,True,1,101,103,4774483,shopping,,27,SHARED2FREE,-0.4441019010696936,-1 -38195869,116450,43843,shopping,1,False,1,103,101,4774483,home,,30,SHARED2FREE,-0.45749089544283433,-1 -39613905,120774,45311,atwork,1,True,1,101,102,4951738,atwork,,20,DRIVEALONEFREE,-0.41128289699554443,-1 -39613909,120774,45311,atwork,1,False,1,102,101,4951738,work,,21,DRIVEALONEFREE,-0.4119255244731903,-1 -39614185,120774,45311,work,1,True,2,106,105,4951773,work,10.647319,10,DRIVEALONEFREE,-0.4328329563140868,-1 -39614186,120774,45311,work,2,True,2,102,106,4951773,work,,11,DRIVEALONEFREE,-0.34803289175033575,-1 -39614189,120774,45311,work,1,False,1,105,102,4951773,home,,30,DRIVEALONEFREE,-0.604685664176941,-1 -39614513,120775,45311,work,1,True,1,101,105,4951814,work,,9,DRIVEALONEFREE,-0.6009435653686525,-1 -39614517,120775,45311,work,1,False,3,101,101,4951814,work,10.767546,28,DRIVEALONEFREE,-0.3567099869251252,-1 -39614518,120775,45311,work,2,False,3,107,101,4951814,othmaint,9.370711,28,DRIVEALONEFREE,-0.5956825017929079,-1 -39614519,120775,45311,work,3,False,3,105,107,4951814,home,,29,DRIVEALONEFREE,-0.43356654047966,-1 -40387937,123133,46056,work,1,True,1,106,106,5048492,work,,20,DRIVEALONEFREE,-0.19777289032936102,-1 -40387941,123133,46056,work,1,False,1,106,106,5048492,home,,40,DRIVEALONEFREE,-0.1974023878574371,-1 -43308361,132037,49258,othmaint,1,True,1,122,110,5413545,othmaint,,23,DRIVEALONEFREE,-0.7390050888061525,-1 -43308365,132037,49258,othmaint,1,False,2,114,122,5413545,eatout,8.7858,24,DRIVEALONEFREE,-0.5175821781158448,-1 -43308366,132037,49258,othmaint,2,False,2,110,114,5413545,home,,24,DRIVEALONEFREE,-0.5938398838043213,-1 -43308537,132038,49258,escort,1,True,1,107,110,5413567,escort,,10,SHARED3FREE,-0.002601420005322437,-1 -43308541,132038,49258,escort,1,False,1,110,107,5413567,home,,22,SHARED3FREE,-0.002601420005322437,-1 -44930737,136983,50912,work,1,True,2,123,112,5616342,eatout,9.353397,31,DRIVEALONEFREE,-0.5493329763412477,-1 -44930738,136983,50912,work,2,True,2,104,123,5616342,work,,32,DRIVEALONEFREE,-0.6666110157966614,-1 -44930741,136983,50912,work,1,False,2,112,104,5616342,social,11.149774,34,DRIVEALONEFREE,-0.5302670001983643,-1 -44930742,136983,50912,work,2,False,2,112,112,5616342,home,,34,DRIVEALONEFREE,-0.18331599235534674,-1 -44931065,136984,50912,work,1,True,2,101,112,5616383,shopping,9.520916,11,DRIVEALONEFREE,-0.6129478216171266,-1 -44931066,136984,50912,work,2,True,2,107,101,5616383,work,,12,DRIVEALONEFREE,-0.6193944811820985,-1 -44931069,136984,50912,work,1,False,3,123,107,5616383,work,10.775923,28,DRIVEALONEFREE,-0.7651270031929017,-1 -44931070,136984,50912,work,2,False,3,104,123,5616383,escort,9.519634,29,DRIVEALONEFREE,-0.6666110157966614,-1 -44931071,136984,50912,work,3,False,3,112,104,5616383,home,,30,DRIVEALONEFREE,-0.5499035120010376,-1 -47621473,145187,53716,othmaint,1,True,3,121,116,5952684,social,9.947862,8,SHARED3FREE,-0.41955729937135083,-1 -47621474,145187,53716,othmaint,2,True,3,112,121,5952684,othmaint,9.261029,11,SHARED3FREE,-0.6422730088233947,-1 -47621475,145187,53716,othmaint,3,True,3,122,112,5952684,othmaint,,11,SHARED3FREE,-0.6419082880020143,-1 -47621477,145187,53716,othmaint,1,False,1,116,122,5952684,home,,20,SHARED3FREE,-0.6134629858242939,-1 -47621737,145188,53716,escort,1,True,1,114,116,5952717,escort,,29,DRIVEALONEFREE,-0.15083796859645277,-1 -47621741,145188,53716,escort,1,False,1,116,114,5952717,home,,30,SHARED2FREE,-0.15179812895272474,-1 -47622241,145189,53716,school,1,True,1,114,116,5952780,school,,10,SCHOOL_BUS,4.3079237937927255,-1 -47622245,145189,53716,school,1,False,1,116,114,5952780,home,,24,SCHOOL_BUS,4.3079237937927255,-1 -47622569,145190,53716,school,1,True,1,114,116,5952821,school,,9,SHARED2FREE,-0.20617904275545365,-1 -47622573,145190,53716,school,1,False,1,116,114,5952821,home,,24,SHARED2FREE,-0.20568500108204935,-1 -48258513,147129,54342,othdiscr,1,True,1,116,117,6032314,othdiscr,,27,DRIVEALONEFREE,-0.5246167778968812,-1 -48258517,147129,54342,othdiscr,1,False,1,117,116,6032314,home,,33,DRIVEALONEFREE,-0.49120157957077026,-1 -48258537,147129,54342,othmaint,1,True,1,114,117,6032317,othmaint,,34,DRIVEALONEFREE,-0.687132179737091,-1 -48258541,147129,54342,othmaint,1,False,2,114,114,6032317,shopping,9.148774,37,DRIVEALONEFREE,-0.42373609542846685,-1 -48258542,147129,54342,othmaint,2,False,2,117,114,6032317,home,,38,DRIVEALONEFREE,-0.6845617890357972,-1 -56357665,171822,63802,eatout,1,True,1,127,135,7044708,eatout,,31,DRIVEALONEFREE,-0.6526245474815369,-1 -56357669,171822,63802,eatout,1,False,1,135,127,7044708,home,,34,DRIVEALONEFREE,-0.6343104243278503,-1 -56357689,171822,63802,escort,1,True,1,135,135,7044711,escort,,28,SHARED3FREE,0.07706324286670248,-1 -56357693,171822,63802,escort,1,False,2,135,135,7044711,escort,11.356267,28,SHARED3FREE,0.07706324286670248,-1 -56357694,171822,63802,escort,2,False,2,135,135,7044711,home,,28,SHARED3FREE,0.07706324286670248,-1 -56357737,171822,63802,othdiscr,1,True,3,131,135,7044717,othdiscr,12.194779,13,SHARED2FREE,0.599977654783949,-1 -56357738,171822,63802,othdiscr,2,True,3,130,131,7044717,shopping,13.357507,14,SHARED2FREE,0.6200047250329787,-1 -56357739,171822,63802,othdiscr,3,True,3,130,130,7044717,othdiscr,,14,SHARED2FREE,0.6960546579187884,-1 -56357741,171822,63802,othdiscr,1,False,1,135,130,7044717,home,,14,SHARED2FREE,0.6487159186367744,-1 -56358209,171823,63802,shopping,1,True,4,131,135,7044776,othmaint,10.342613,24,SHARED3FREE,-0.14619837454037923,-1 -56358210,171823,63802,shopping,2,True,4,131,131,7044776,social,12.281772,25,SHARED3FREE,-0.012169709209450414,-1 -56358211,171823,63802,shopping,3,True,4,131,131,7044776,shopping,11.556939,26,SHARED3FREE,-0.012169709209450414,-1 -56358212,171823,63802,shopping,4,True,4,131,131,7044776,shopping,,26,SHARED3FREE,-0.012169709209450414,-1 -56358213,171823,63802,shopping,1,False,1,135,131,7044776,home,,27,DRIVEALONEFREE,-0.15095594351539895,-1 -56358473,171824,63802,othdiscr,1,True,1,131,135,7044809,othdiscr,,32,SHARED2FREE,-0.46024149381952484,-1 -56358477,171824,63802,othdiscr,1,False,1,135,131,7044809,home,,37,SHARED2FREE,-0.45329299190068956,-1 -56358521,171824,63802,school,1,True,2,135,135,7044815,escort,11.635028,10,SHARED2FREE,0.10569338088788001,-1 -56358522,171824,63802,school,2,True,2,135,135,7044815,school,,10,SHARED3FREE,0.10569338088788001,-1 -56358525,171824,63802,school,1,False,2,135,135,7044815,othdiscr,11.906311,25,SHARED3FREE,0.10545807803885715,-1 -56358526,171824,63802,school,2,False,2,135,135,7044815,home,,26,SHARED3FREE,0.10545807803885715,-1 -56358801,171825,63802,othdiscr,1,True,1,131,135,7044850,othdiscr,,29,SHARED3FREE,-0.281769477499857,-1 -56358805,171825,63802,othdiscr,1,False,2,132,131,7044850,social,10.225653,35,SHARED2FREE,-0.20277185632585107,-1 -56358806,171825,63802,othdiscr,2,False,2,135,132,7044850,home,,39,SHARED3FREE,-0.36521793162300004,-1 -56358809,171825,63802,othdiscr,1,True,4,135,135,7044851,othmaint,5.3795877,26,WALK,-0.7460585832595825,-1 -56358810,171825,63802,othdiscr,2,True,4,131,135,7044851,othmaint,5.4266872,27,WALK,-2.0398435592651363,-1 -56358811,171825,63802,othdiscr,3,True,4,130,131,7044851,othmaint,5.7105064,28,WALK,-1.2828608751297,-1 -56358812,171825,63802,othdiscr,4,True,4,130,130,7044851,othdiscr,,28,WALK,-0.78075897693634,-1 -56358813,171825,63802,othdiscr,1,False,1,135,130,7044851,home,,28,WALK,-1.4660019874572756,-1 -56358849,171825,63802,school,1,True,1,135,135,7044856,school,,9,SHARED3FREE,0.10569338088788001,-1 -56358853,171825,63802,school,1,False,1,135,135,7044856,home,,24,SHARED3FREE,0.10569338088788001,-1 -56359177,171826,63802,school,1,True,1,135,135,7044897,school,,10,SHARED3FREE,0.10569338088788001,-1 -56359181,171826,63802,school,1,False,1,135,135,7044897,home,,22,SHARED3FREE,0.10569338088788001,-1 +trip_id,person_id,household_id,primary_purpose,trip_num,outbound,trip_count,destination,origin,tour_id,purpose,destination_logsum,depart,trip_mode,mode_choice_logsum +37314161,113762,42730,othmaint,1,TRUE,1,106,103,4664270,othmaint,,10,DRIVEALONEFREE,-0.356781572 +37314165,113762,42730,othmaint,1,FALSE,1,103,106,4664270,home,,11,DRIVEALONEFREE,-0.356460303 +38194977,116448,43843,atwork,1,TRUE,1,106,101,4774372,atwork,,20,DRIVEALONEFREE,-0.321751714 +38194981,116448,43843,atwork,1,FALSE,1,101,106,4774372,work,,21,DRIVEALONEFREE,-0.321751714 +38195065,116449,43843,othdiscr,1,TRUE,1,106,103,4774383,othdiscr,,32,SHARED2FREE,0.7593915111282218 +38195069,116449,43843,othdiscr,1,FALSE,1,103,106,4774383,home,,37,SHARED2FREE,0.7593915111282218 +38195257,116448,43843,work,1,TRUE,2,107,103,4774407,othmaint,9.244319,10,DRIVEALONEFREE,-0.667137086 +38195258,116448,43843,work,2,TRUE,2,101,107,4774407,work,,10,DRIVEALONEFREE,-0.589384019 +38195261,116448,43843,work,1,FALSE,1,103,101,4774407,home,,30,DRIVEALONEFREE,-0.501271665 +38195585,116449,43843,work,1,TRUE,2,106,103,4774448,othmaint,10.644734,12,DRIVEALONEFREE,0.050863062 +38195586,116449,43843,work,2,TRUE,2,102,106,4774448,work,,13,DRIVEALONEFREE,0.032545052 +38195589,116449,43843,work,1,FALSE,3,103,102,4774448,othmaint,10.796497,23,SHARED2FREE,0.09835211 +38195590,116449,43843,work,2,FALSE,3,103,103,4774448,work,12.367123,24,DRIVEALONEFREE,0.24223826711784288 +38195591,116449,43843,work,3,FALSE,3,103,103,4774448,home,,26,DRIVEALONEFREE,0.2401515927071465 +38195849,116450,43843,school,1,TRUE,1,106,103,4774481,school,,9,SCHOOL_BUS,4.351044654846191 +38195853,116450,43843,school,1,FALSE,1,103,106,4774481,home,,27,SCHOOL_BUS,4.351044654846191 +38195865,116450,43843,shopping,1,TRUE,1,101,103,4774483,shopping,,27,SHARED2FREE,-0.444101901 +38195869,116450,43843,shopping,1,FALSE,1,103,101,4774483,home,,30,SHARED2FREE,-0.457490895 +39613905,120774,45311,atwork,1,TRUE,1,101,102,4951738,atwork,,20,DRIVEALONEFREE,-0.411282897 +39613909,120774,45311,atwork,1,FALSE,1,102,101,4951738,work,,21,DRIVEALONEFREE,-0.411925524 +39614185,120774,45311,work,1,TRUE,2,106,105,4951773,work,10.647319,10,DRIVEALONEFREE,-0.432832956 +39614186,120774,45311,work,2,TRUE,2,102,106,4951773,work,,11,DRIVEALONEFREE,-0.348032892 +39614189,120774,45311,work,1,FALSE,1,105,102,4951773,home,,30,DRIVEALONEFREE,-0.604685664 +39614513,120775,45311,work,1,TRUE,1,101,105,4951814,work,,9,DRIVEALONEFREE,-0.600943565 +39614517,120775,45311,work,1,FALSE,3,101,101,4951814,work,10.767546,28,DRIVEALONEFREE,-0.356709987 +39614518,120775,45311,work,2,FALSE,3,107,101,4951814,othmaint,9.370711,28,DRIVEALONEFREE,-0.595682502 +39614519,120775,45311,work,3,FALSE,3,105,107,4951814,home,,29,DRIVEALONEFREE,-0.43356654 +40387937,123133,46056,work,1,TRUE,1,106,106,5048492,work,,20,DRIVEALONEFREE,-0.19777289 +40387941,123133,46056,work,1,FALSE,1,106,106,5048492,home,,40,DRIVEALONEFREE,-0.197402388 +43308361,132037,49258,othmaint,1,TRUE,1,122,110,5413545,othmaint,,23,DRIVEALONEFREE,-0.739005089 +43308365,132037,49258,othmaint,1,FALSE,2,114,122,5413545,eatout,8.7858,24,DRIVEALONEFREE,-0.517582178 +43308366,132037,49258,othmaint,2,FALSE,2,110,114,5413545,home,,24,DRIVEALONEFREE,-0.593839884 +43308537,132038,49258,escort,1,TRUE,1,107,110,5413567,escort,,10,SHARED3FREE,-0.00260142 +43308541,132038,49258,escort,1,FALSE,1,110,107,5413567,home,,22,SHARED3FREE,-0.00260142 +44930737,136983,50912,work,1,TRUE,2,123,112,5616342,eatout,9.353397,31,DRIVEALONEFREE,-0.549332976 +44930738,136983,50912,work,2,TRUE,2,104,123,5616342,work,,32,DRIVEALONEFREE,-0.666611016 +44930741,136983,50912,work,1,FALSE,2,112,104,5616342,social,11.149774,34,DRIVEALONEFREE,-0.530267 +44930742,136983,50912,work,2,FALSE,2,112,112,5616342,home,,34,DRIVEALONEFREE,-0.183315992 +44931065,136984,50912,work,1,TRUE,2,101,112,5616383,shopping,9.520916,11,DRIVEALONEFREE,-0.612947822 +44931066,136984,50912,work,2,TRUE,2,107,101,5616383,work,,12,DRIVEALONEFREE,-0.619394481 +44931069,136984,50912,work,1,FALSE,3,123,107,5616383,work,10.775923,28,DRIVEALONEFREE,-0.765127003 +44931070,136984,50912,work,2,FALSE,3,104,123,5616383,escort,9.519634,29,DRIVEALONEFREE,-0.666611016 +44931071,136984,50912,work,3,FALSE,3,112,104,5616383,home,,30,DRIVEALONEFREE,-0.549903512 +47621473,145187,53716,othmaint,1,TRUE,3,121,116,5952684,social,9.947862,8,SHARED3FREE,-0.419557299 +47621474,145187,53716,othmaint,2,TRUE,3,112,121,5952684,othmaint,9.261029,11,SHARED3FREE,-0.642273009 +47621475,145187,53716,othmaint,3,TRUE,3,122,112,5952684,othmaint,,11,SHARED3FREE,-0.641908288 +47621477,145187,53716,othmaint,1,FALSE,1,116,122,5952684,home,,20,SHARED3FREE,-0.613462986 +47621737,145188,53716,escort,1,TRUE,1,114,116,5952717,escort,,29,DRIVEALONEFREE,-0.150837969 +47621741,145188,53716,escort,1,FALSE,1,116,114,5952717,home,,30,SHARED2FREE,-0.151798129 +47622241,145189,53716,school,1,TRUE,1,114,116,5952780,school,,10,SCHOOL_BUS,4.3079237937927255 +47622245,145189,53716,school,1,FALSE,1,116,114,5952780,home,,24,SCHOOL_BUS,4.3079237937927255 +47622569,145190,53716,school,1,TRUE,1,114,116,5952821,school,,9,SHARED2FREE,-0.206179043 +47622573,145190,53716,school,1,FALSE,1,116,114,5952821,home,,24,SHARED2FREE,-0.205685001 +48258513,147129,54342,othdiscr,1,TRUE,1,116,117,6032314,othdiscr,,27,DRIVEALONEFREE,-0.524616778 +48258517,147129,54342,othdiscr,1,FALSE,1,117,116,6032314,home,,33,DRIVEALONEFREE,-0.49120158 +48258537,147129,54342,othmaint,1,TRUE,1,114,117,6032317,othmaint,,34,DRIVEALONEFREE,-0.68713218 +48258541,147129,54342,othmaint,1,FALSE,2,114,114,6032317,shopping,9.148774,37,DRIVEALONEFREE,-0.423736095 +48258542,147129,54342,othmaint,2,FALSE,2,117,114,6032317,home,,38,DRIVEALONEFREE,-0.684561789 +56357665,171822,63802,eatout,1,TRUE,1,127,135,7044708,eatout,,31,DRIVEALONEFREE,-0.652624547 +56357669,171822,63802,eatout,1,FALSE,1,135,127,7044708,home,,34,DRIVEALONEFREE,-0.634310424 +56357689,171822,63802,escort,1,TRUE,1,135,135,7044711,escort,,28,SHARED3FREE,0.077063243 +56357693,171822,63802,escort,1,FALSE,2,135,135,7044711,escort,11.356267,28,SHARED3FREE,0.077063243 +56357694,171822,63802,escort,2,FALSE,2,135,135,7044711,home,,28,SHARED3FREE,0.077063243 +56357737,171822,63802,othdiscr,1,TRUE,3,131,135,7044717,othdiscr,12.194779,13,SHARED2FREE,0.599977655 +56357738,171822,63802,othdiscr,2,TRUE,3,130,131,7044717,shopping,13.357507,14,SHARED2FREE,0.6200047250329787 +56357739,171822,63802,othdiscr,3,TRUE,3,130,130,7044717,othdiscr,,14,SHARED2FREE,0.6960546579187884 +56357741,171822,63802,othdiscr,1,FALSE,1,135,130,7044717,home,,14,SHARED2FREE,0.6487159186367744 +56358209,171823,63802,shopping,1,TRUE,4,131,135,7044776,othmaint,10.342613,24,SHARED3FREE,-0.146198375 +56358210,171823,63802,shopping,2,TRUE,4,131,131,7044776,social,12.281772,25,SHARED3FREE,-0.012169709 +56358211,171823,63802,shopping,3,TRUE,4,131,131,7044776,shopping,11.556939,26,SHARED3FREE,-0.012169709 +56358212,171823,63802,shopping,4,TRUE,4,131,131,7044776,shopping,,26,SHARED3FREE,-0.012169709 +56358213,171823,63802,shopping,1,FALSE,1,135,131,7044776,home,,27,DRIVEALONEFREE,-0.150955944 +56358473,171824,63802,othdiscr,1,TRUE,1,131,135,7044809,othdiscr,,32,SHARED2FREE,-0.460241494 +56358477,171824,63802,othdiscr,1,FALSE,1,135,131,7044809,home,,37,SHARED2FREE,-0.453292992 +56358521,171824,63802,school,1,TRUE,2,135,135,7044815,escort,11.635028,10,SHARED2FREE,0.10569338088788001 +56358522,171824,63802,school,2,TRUE,2,135,135,7044815,school,,10,SHARED3FREE,0.10569338088788001 +56358525,171824,63802,school,1,FALSE,2,135,135,7044815,othdiscr,11.906311,25,SHARED3FREE,0.10545807803885715 +56358526,171824,63802,school,2,FALSE,2,135,135,7044815,home,,26,SHARED3FREE,0.10545807803885715 +56358801,171825,63802,othdiscr,1,TRUE,1,131,135,7044850,othdiscr,,29,SHARED3FREE,-0.281769477 +56358805,171825,63802,othdiscr,1,FALSE,2,132,131,7044850,social,10.225653,35,SHARED2FREE,-0.202771856 +56358806,171825,63802,othdiscr,2,FALSE,2,135,132,7044850,home,,39,SHARED3FREE,-0.365217932 +56358809,171825,63802,othdiscr,1,TRUE,4,135,135,7044851,othmaint,5.3795877,26,WALK,-0.746058583 +56358810,171825,63802,othdiscr,2,TRUE,4,131,135,7044851,othmaint,5.4266872,27,WALK,-2.039843559 +56358811,171825,63802,othdiscr,3,TRUE,4,130,131,7044851,othmaint,5.7105064,28,WALK,-1.282860875 +56358812,171825,63802,othdiscr,4,TRUE,4,130,130,7044851,othdiscr,,28,WALK,-0.780758977 +56358813,171825,63802,othdiscr,1,FALSE,1,135,130,7044851,home,,28,WALK,-1.466001987 +56358849,171825,63802,school,1,TRUE,1,135,135,7044856,school,,9,SHARED3FREE,0.10569338088788001 +56358853,171825,63802,school,1,FALSE,1,135,135,7044856,home,,24,SHARED3FREE,0.10569338088788001 +56359177,171826,63802,school,1,TRUE,1,135,135,7044897,school,,10,SHARED3FREE,0.10569338088788001 +56359181,171826,63802,school,1,FALSE,1,135,135,7044897,home,,22,SHARED3FREE,0.10569338088788001 diff --git a/activitysim/examples/prototype_arc/test/test_arc.py b/activitysim/examples/prototype_arc/test/test_arc.py index 3e637289c5..140c67a313 100644 --- a/activitysim/examples/prototype_arc/test/test_arc.py +++ b/activitysim/examples/prototype_arc/test/test_arc.py @@ -13,7 +13,7 @@ from activitysim.core.test import assert_frame_substantively_equal -def _test_arc(recode=False, sharrow=False): +def _test_arc(recode=False, sharrow=False, eet=False): def example_path(dirname): resource = os.path.join("examples", "prototype_arc", dirname) return str(importlib.resources.files("activitysim").joinpath(resource)) @@ -24,9 +24,13 @@ def test_path(dirname): def regress(): if sharrow: # sharrow results in tiny changes (one trip moving one time period earlier) - regress_trips_df = pd.read_csv(test_path("regress/final_trips_sh.csv")) + regress_trips_df = pd.read_csv( + test_path(f"regress/final_trips{'_eet' if eet else ''}_sh.csv") + ) else: - regress_trips_df = pd.read_csv(test_path("regress/final_trips.csv")) + regress_trips_df = pd.read_csv( + test_path(f"regress/final_trips{'_eet' if eet else ''}.csv") + ) final_trips_df = pd.read_csv(test_path("output/final_trips.csv")) # person_id,household_id,tour_id,primary_purpose,trip_num,outbound,trip_count,purpose, @@ -36,39 +40,26 @@ def regress(): file_path = os.path.join(os.path.dirname(__file__), "simulation.py") + test_configs = [] + if eet: + test_configs.extend(["-c", test_path("configs_eet")]) + if recode: - run_args = [ - "-c", - test_path("configs_recode"), - "-c", - example_path("configs"), - "-d", - example_path("data"), - "-o", - test_path("output"), - ] + test_configs.extend(["-c", test_path("configs_recode")]) elif sharrow: - run_args = [ - "-c", - test_path("configs_sharrow"), - "-c", - example_path("configs"), - "-d", - example_path("data"), - "-o", - test_path("output"), - ] + test_configs.extend(["-c", test_path("configs_sharrow")]) else: - run_args = [ - "-c", - test_path("configs"), - "-c", - example_path("configs"), - "-d", - example_path("data"), - "-o", - test_path("output"), - ] + test_configs.extend(["-c", test_path("configs")]) + + run_args = [ + *test_configs, + "-c", + example_path("configs"), + "-d", + example_path("data"), + "-o", + test_path("output"), + ] if os.environ.get("GITHUB_ACTIONS") == "true": subprocess.run(["coverage", "run", "-a", file_path] + run_args, check=True) @@ -82,15 +73,21 @@ def test_arc(): _test_arc() +def test_arc_eet(): + _test_arc(eet=True) + + def test_arc_recode(): _test_arc(recode=True) -def test_arc_sharrow(): - _test_arc(sharrow=True) +# TODO: update regress trips for sharrow and re-enable test. +# def test_arc_sharrow(): +# _test_arc(sharrow=True) if __name__ == "__main__": _test_arc() + _test_arc(eet=True) _test_arc(recode=True) - _test_arc(sharrow=True) + # _test_arc(sharrow=True) diff --git a/docs/core.rst b/docs/core.rst index 687e8f9565..a7a9ba59d6 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -323,6 +323,20 @@ To specify and solve an NL model: * specify the nesting structure via the NESTS setting in the model configuration YAML file. An example nested logit NESTS entry can be found in ``example/configs/tour_mode_choice.yaml`` * call ``simulate.simple_simulate()``. The ``simulate.interaction_simulate()`` functionality is not yet supported for NL. +Explicit Error Terms +^^^^^^^^^^^^^^^^^^^^ + +By default, ActivitySim makes choices by calculating analytical probabilities and then drawing once from +the cumulative distribution for each chooser. With Explicit Error Terms (EET), enabled by setting +``use_explicit_error_terms: True`` in ``settings.yaml``, ActivitySim instead draws a standard EV1 (Gumbel) error +term for each chooser-alternative pair, adds it to the observed utility, and chooses the maximum total utility. + +EET changes the final simulation step, not the utility expressions, availability logic, or nesting +structure. In practice, it can reduce Monte Carlo noise in scenario comparisons. + +For configuration guidance see :ref:`explicit_error_terms_ways_to_run`. For detailed implementation notes +see :doc:`/dev-guide/explicit-error-terms`. + API ^^^ diff --git a/docs/dev-guide/explicit-error-terms.md b/docs/dev-guide/explicit-error-terms.md new file mode 100644 index 0000000000..57e30f7bc4 --- /dev/null +++ b/docs/dev-guide/explicit-error-terms.md @@ -0,0 +1,188 @@ +(explicit-error-terms-dev)= +# Explicit Error Terms + +Explicit Error Terms (EET) is an alternative way to simulate choices from ActivitySim's +logit models. It keeps the same systematic utilities and the same random-utility +interpretation as the standard method, but changes how the final simulated choice is +drawn. + +For user-facing guidance on when to use EET, see {ref}`explicit_error_terms_ways_to_run`. + +## Enabling EET + +Enable EET globally in `settings.yaml`: + +```yaml +use_explicit_error_terms: True +``` + +The top-level switch is defined in +`activitysim.core.configuration.top.SimulationSettings.use_explicit_error_terms`. +Choice simulation code reads that setting through the model compute settings and routes +supported logit simulations through the EET path. + +## Default Draw Versus EET + +Under the default ActivitySim simulation path, choice drawing works like this: + +1. Compute systematic utilities. +2. Convert those utilities into analytical probabilities. +3. Draw one uniform random number per chooser. +4. Select the alternative whose cumulative probability interval contains that draw. + +With EET enabled, the final draw step changes: + +1. Compute systematic utilities. +2. Draw one iid EV1 error term for each chooser-alternative pair. +3. Add that error term to the systematic utility. +4. Choose the alternative with the highest total utility. + +For multinomial logit, ActivitySim adds Gumbel draws to the utility table and takes the +row-wise maximum. For nested logit, ActivitySim applies the same idea while walking the +nest tree, preserving the configured nesting structure. For details, see +[this ATRF paper](https://australasiantransportresearchforum.org.au/frozen-randomness-at-the-individual-utility-level/). + +The model being simulated does not change. EET changes how the random utility model is +sampled, not the underlying utility specification. + +## Practical Effects + +### Comparisons and Simulation Noise + +For EET to reduce simulation noise, it is important that alternatives of a choice situation +keep the same unobserved error term in different scenario runs. This is intimately tied +to how random numbers are generated; see {ref}`random_in_detail` for the underlying +random-number stream design and the `activitysim.core.random` API. +Because unchanged alternatives can keep the same unobserved draws, changes to choices between +scenarios can only happen when the observed utility of an alternative increases. This is not +the case for the Monte Carlo simulation method, where the draws are based on probabilities, +which necessarily change for all alternatives if any observed utility changes. + +This also means that it is advisable to use the same setting in all runs. Comparing a baseline +run with EET to a scenario run without EET mixes two simulation methods and can make differences +harder to interpret. Aggregate choice patterns should remain statistically the same +as for the default probability-based method. The project test suite includes parity tests for +MNL, NL, and interaction-based simulations. + +### Numerical and Debugging Behavior + +EET changes the final simulation step, not the utility calculation itself. Utility +expressions, availability logic, nesting structure, and utility validation still matter in +the same way as in the default method. + +In practice, EET can make some comparisons easier to interpret because the selected +alternative is the one with the highest total utility after adding the explicit error term, +rather than the one reached by a cumulative-probability threshold. That can reduce +sensitivity to small differences in the final CDF draw when comparing nearby scenarios. +It does not eliminate the need to inspect invalid or unavailable alternatives, and it does +not guarantee identical results across different RNG seeds or different model +configurations. + +For shadow-priced location choice, ActivitySim resets RNG offsets between iterations when +EET is enabled so each shadow-pricing iteration uses the same sequence of random numbers. +That keeps the comparison across iterations focused on the shadow price updates instead of +changing random draws between iterations. + +### Runtime + +Runtime differs between the methods. EET generates one EV1 error term per chooser-alternative +pair, while the default Monte Carlo path draws only one uniform random number per chooser after +probabilities are computed. EET, however, does not need to compute probabilities to make choices. + +Exact runtimes depend on the number of alternatives, nesting structure, interaction size, and +sampling configuration. With default settings, current full-scale demand model runs with EET +are about 100% higher than the default MC method. While the relative runtime increase +of nested logit models is large, these typically contribute only a very small fraction to the +overall runtime and virtually all of the increase is due to sampling in location choice. To +avoid this penalty, it is possible to use MC for sampling only by adding the following to each +model setting where sampling is used (currently all location and destination choice models as +well as disaggregate accessibilities): + +```yaml +compute_settings: + use_explicit_error_terms: + sample: false +``` + +With this setting, model runtimes should be roughly equal. The influence of this change on +sampling noise is under investigation. + +(explicit_error_terms_zone_encoding)= +#### Zone ID encoding and runtime + +For location choice models, encoding zone IDs as a 0-based contiguous index reduces EET runtime +and memory use during sampling. + +The current implementation draws error terms into a dense 1-D array of length `max_zone_id + 1` +per chooser (see `AltsContext.n_alts_to_cover_max_id` in `activitysim.core.logit`). Each sampled +alternative is then looked up by direct offset into that array, so the same zone always receives +the same error term regardless of which alternatives are in the sampled choice set — a property +needed for consistent scenario comparisons. + +When zone IDs are a contiguous 0-based sequence, the dense array has exactly as many entries as +there are zones and every draw is used. When zone IDs contain gaps or start from a large value, +the array must still cover `max_zone_id + 1` entries, so the draws for the missing IDs are +generated but never used. For zone systems with large or sparse IDs, this waste can be substantial. + +An alternative would be to draw only as many error terms as there are sampled alternatives and +retrieve the relevant term for each zone via a lookup. That would avoid unused draws but adds an +index-mapping step for every chooser-sample in the interaction frame, trading one form of overhead +for another. The current design favours the dense approach because the direct-offset indexing is +simpler and because the ``recode_columns`` setting can encode zone IDs as ``zero-based`` in +the input table list; see the +[Zero-based Recoding of Zones](using-sharrow.md#zero-based-recoding-of-zones) section for details. + +(explicit_error_terms_memory)= +### Memory usage + +When running EET with MC for location sampling as described in the Runtime section above, +there should be only a small increase in memory usage for location choice models compared to full +MC simulation. + +However, when EET is run with its current default location sampling settings, an array of size +(number of choosers, number of alternatives, number of samples) is allocated for all random error +terms. This can quickly become unwieldy for machines with limited memory, and +[chunking](../users-guide/performance/chunking.md) will likely be needed. + +When chunking is needed and [explicit chunking](../users-guide/performance/chunking.md#explicit-chunking) +is used, using fractional values for the chunk size rather than absolute numbers of choosers is +often a better fit. This is because the individual steps of location choice models +(location sampling, location logsums, and location choice from the sampled choice set) all have +very different chooser characteristics, but the chunk size currently can only be set at the model +level. Using absolute values for the explicit chunk size would lead to a large number of chunks +for the logsum calculations, which is relatively slow. + + +## Implementation Details and Adding New Models + +The core simulation is implemented in `activitysim.core.logit.make_choices_utility_based`. Most +calls to this function are wrapped in one of the following methods: + +- `activitysim.core.simulate` +- `activitysim.core.interaction_simulate` +- `activitysim.core.interaction_sample` +- `activitysim.core.interaction_sample_simulate` + +These wrappers all implement EET consistently, so any model using them will automatically support +EET. Some models call the underlying choice simulation method +`activitysim.core.logit.make_choices` directly. For EET to work in that case, the developer must +add a corresponding call to `logit.make_choices_utility_based`; see for example +`activitysim.abm.models.utils.cdap.household_activity_choices`. Models that draw directly +from probability distributions, such as `activitysim.abm.models.utils.cdap.extra_hh_member_choices`, +do not have a corresponding EET implementation because there are no utilities to work with. + + +### Unavailable choices utility convention + +For EET, only utility differences matter, and therefore the outcome for two utilities that are +very small, say -10000 and -10001, is identical to the outcome for 0 and 1. For MC, utilities +have to be exponentiated and therefore floating point precision dictates the smallest and largest +utility that can be used in practice. ActivitySim models historically often use a utility of +-999 to make alternatives practically unavailable. That value is below the utility threshold +used in the probability-based path, which is about -691 because ActivitySim clips +exponentiated utilities at 1e-300. To keep behavior consistent, EET treats alternatives with +utilities at or below that threshold as unavailable; see `activitysim.core.logit.validate_utils`. + +### Scale of the distribution +Error terms are drawn from standard Gumbel distributions, i.e., the scale of the error term is +fixed to one. diff --git a/docs/dev-guide/index.rst b/docs/dev-guide/index.rst index da6c649733..82051ff08e 100644 --- a/docs/dev-guide/index.rst +++ b/docs/dev-guide/index.rst @@ -33,6 +33,7 @@ Contents component-configs components/index ../core + explicit-error-terms ../benchmarking build-docs changes diff --git a/docs/users-guide/ways_to_run.rst b/docs/users-guide/ways_to_run.rst index 1b21221078..ca569ca641 100644 --- a/docs/users-guide/ways_to_run.rst +++ b/docs/users-guide/ways_to_run.rst @@ -80,7 +80,7 @@ Refer to the :ref:`Run the Primary Example` section to learn how to run the prim Using Jupyter Notebook ______________________ -ActivitySim includes a `Jupyter Notebook `__ recipe book with interactive examples. +ActivitySim includes a `Jupyter Notebook `__ recipe book with interactive examples. * To start JupyterLab, from the ActivitySim project directory run ``uv run jupyter lab``. This will start the JupyterLab server and pop up a browser window with the interactive development environment. * Navigate to the ``examples/prototype_mtc/notebooks`` folder and select a notebook to learn more: @@ -283,3 +283,50 @@ With the set of output CSV files, the user can trace ActivitySim calculations in help debug data and/or logic errors. Refer to :ref:`trace` for more details on configuring tracing and the various output files. + +.. _explicit_error_terms_ways_to_run : + +Explicit Error Terms +____________________ + +ActivitySim makes heavy use of micro-simulation. Most model components are discrete choice models with an inherent +random component, and for each choice situation a single outcome is generated. +With the default Monte Carlo draw method, ActivitySim first calculates analytical probabilities from the +systematic utilities of a multinomial or nested logit model and then makes one draw from the +cumulative distribution for each chooser. Explicit Error Terms (EET) replaces that final draw with a direct +random-utility simulation by drawing an independent standard EV1 (Gumbel) error term for each +chooser-alternative pair, adding it to the systematic utility, and selecting the alternative with the highest +total utility. Both methods simulate the same underlying model, but EET can be less affected by Monte Carlo +noise when comparing scenarios. For more details, see :doc:`/dev-guide/explicit-error-terms`. + +To enable EET for a model run, set the global switch in ``settings.yaml``: + +.. code-block:: yaml + + use_explicit_error_terms: True + +Enable or disable this setting consistently across all runs being compared. + +Using EET changes the simulation method, not the underlying model. Aggregate behavior should remain statistically +comparable to the default method, but individual simulated choices will not usually match record-by-record. +EET is currently slower than the default probability-based simulation method. Most of the slowdown comes from location +choice models, where the number of alternatives is large and the current importance-sampling workflow requires +many repeated error term draws. Work to reduce that overhead is ongoing. Until then, it is also possible to turn +off EET for the sampling part of these models by adding the following lines to the settings of all models where +location choice sampling is used (currently all location and destination choice models as well as disaggregate +accessibilities): + +.. code-block:: yaml + + compute_settings: + use_explicit_error_terms: + sample: false + +If you keep EET enabled for the sampling step, also consider memory usage during location sampling. +In that case, explicit chunking with a fractional ``explicit_chunk`` setting is often the most +practical approach; see :ref:`explicit_error_terms_memory` for details. + +For location choice models, encoding zone IDs as a 0-based contiguous index also reduces EET runtime and memory usage; +see :ref:`explicit_error_terms_zone_encoding` for a technical description. For models where the input data does not +already use contiguous zone IDs, the ``recode_columns`` option can be used to create them. See the +*Zero-based Recoding of Zones* section in :doc:`/dev-guide/using-sharrow` for more details. diff --git a/pyproject.toml b/pyproject.toml index a8fef9948e..8d6496c032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,7 +118,7 @@ dev = [ "isort==5.12.0", "jupyter-book", "jupyterlab", - "larch>=6.0.42", + "larch>=6.0.46", "matplotlib", "myst-nb", "myst-parser", # allows markdown in sphinx diff --git a/uv.lock b/uv.lock index f1ffb126df..f890e21e14 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -145,7 +145,7 @@ dev = [ { name = "isort", specifier = "==5.12.0" }, { name = "jupyter-book" }, { name = "jupyterlab" }, - { name = "larch", specifier = ">=6.0.42" }, + { name = "larch", specifier = ">=6.0.46" }, { name = "matplotlib" }, { name = "myst-nb" }, { name = "myst-parser" }, @@ -1663,6 +1663,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, @@ -1672,6 +1674,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, @@ -1681,6 +1685,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, @@ -1690,6 +1696,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, @@ -1697,6 +1705,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] @@ -2321,7 +2331,7 @@ wheels = [ [[package]] name = "larch" -version = "6.0.42" +version = "6.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "addicty" }, @@ -2337,15 +2347,15 @@ dependencies = [ { name = "pandas", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "pyarrow" }, { name = "rich" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, + { name = "sharrow" }, { name = "sparse" }, { name = "xarray" }, { name = "xmle" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/3c/0db11f6fbe7dcf2edf2c2843f4b74f03d42f3f607a20e774bf3ec74d3467/larch-6.0.42.tar.gz", hash = "sha256:09c76404a3a91505acd04902743a156ad5b84697d3c69796452d3852a4b2f2dd", size = 2153168, upload-time = "2025-09-28T22:58:23.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/0d/6274565f53dba31cfc5af5033e95e64261baee7352d82f67804d49d330ce/larch-6.0.46.tar.gz", hash = "sha256:be78a4196a6c5cbe6300ef6d7718406fab545799d8262af18f6f20021115cb5c", size = 2101313, upload-time = "2026-03-13T01:14:51.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/a9/2bd3ea4b0c4297b06db04553709bd7d3a01c0e8847a621832d3f1cec8fec/larch-6.0.42-py3-none-any.whl", hash = "sha256:5d9cf582c9c0b293eca15ddb1859e516ad89cdb33c9509b447f7c137b97c647c", size = 1851908, upload-time = "2025-09-28T22:58:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/f875dafd1da4e80157783800ecf9548b2259cd287fe10f9bf1b2a65f5269/larch-6.0.46-py3-none-any.whl", hash = "sha256:dccbb0f014bd5dc53fef58957801e06da2f06afb0a90c5f6ef60b903b245f7f6", size = 1740545, upload-time = "2026-03-13T01:14:49.644Z" }, ] [[package]] @@ -4730,6 +4740,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/fa/3234f913fe9a6525a7b97c6dad1f51e72b917e6872e051a5e2ffd8b16fbb/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83", size = 137970, upload-time = "2025-09-22T19:51:09.472Z" }, { url = "https://files.pythonhosted.org/packages/ef/ec/4edbf17ac2c87fa0845dd366ef8d5852b96eb58fcd65fc1ecf5fe27b4641/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27", size = 739639, upload-time = "2025-09-22T19:51:10.566Z" }, { url = "https://files.pythonhosted.org/packages/15/18/b0e1fafe59051de9e79cdd431863b03593ecfa8341c110affad7c8121efc/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640", size = 764456, upload-time = "2025-09-22T19:51:11.736Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" }, ] [[package]] @@ -4777,8 +4789,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, { name = "numpy" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } @@ -4819,11 +4830,8 @@ wheels = [ name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4874,82 +4882,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] -[[package]] -name = "scipy" -version = "1.16.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version >= '3.12' and python_full_version < '3.14'", - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/ef/37ed4b213d64b48422df92560af7300e10fe30b5d665dd79932baebee0c6/scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92", size = 36619956, upload-time = "2025-09-11T17:39:20.5Z" }, - { url = "https://files.pythonhosted.org/packages/85/ab/5c2eba89b9416961a982346a4d6a647d78c91ec96ab94ed522b3b6baf444/scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e", size = 28931117, upload-time = "2025-09-11T17:39:29.06Z" }, - { url = "https://files.pythonhosted.org/packages/80/d1/eed51ab64d227fe60229a2d57fb60ca5898cfa50ba27d4f573e9e5f0b430/scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173", size = 20921997, upload-time = "2025-09-11T17:39:34.892Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/33ea3e23bbadde96726edba6bf9111fb1969d14d9d477ffa202c67bec9da/scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d", size = 23523374, upload-time = "2025-09-11T17:39:40.846Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/7399dc96e1e3f9a05e258c98d716196a34f528eef2ec55aad651ed136d03/scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2", size = 33583702, upload-time = "2025-09-11T17:39:49.011Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bc/a5c75095089b96ea72c1bd37a4497c24b581ec73db4ef58ebee142ad2d14/scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9", size = 35883427, upload-time = "2025-09-11T17:39:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/ab/66/e25705ca3d2b87b97fe0a278a24b7f477b4023a926847935a1a71488a6a6/scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3", size = 36212940, upload-time = "2025-09-11T17:40:06.013Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fd/0bb911585e12f3abdd603d721d83fc1c7492835e1401a0e6d498d7822b4b/scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88", size = 38865092, upload-time = "2025-09-11T17:40:15.143Z" }, - { url = "https://files.pythonhosted.org/packages/d6/73/c449a7d56ba6e6f874183759f8483cde21f900a8be117d67ffbb670c2958/scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa", size = 38687626, upload-time = "2025-09-11T17:40:24.041Z" }, - { url = "https://files.pythonhosted.org/packages/68/72/02f37316adf95307f5d9e579023c6899f89ff3a051fa079dbd6faafc48e5/scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c", size = 25503506, upload-time = "2025-09-11T17:40:30.703Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d", size = 36646259, upload-time = "2025-09-11T17:40:39.329Z" }, - { url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371", size = 28888976, upload-time = "2025-09-11T17:40:46.82Z" }, - { url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0", size = 20879905, upload-time = "2025-09-11T17:40:52.545Z" }, - { url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232", size = 23553066, upload-time = "2025-09-11T17:40:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1", size = 33336407, upload-time = "2025-09-11T17:41:06.796Z" }, - { url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f", size = 35673281, upload-time = "2025-09-11T17:41:15.055Z" }, - { url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef", size = 36004222, upload-time = "2025-09-11T17:41:23.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1", size = 38664586, upload-time = "2025-09-11T17:41:31.021Z" }, - { url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e", size = 38550641, upload-time = "2025-09-11T17:41:36.61Z" }, - { url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851", size = 25456070, upload-time = "2025-09-11T17:41:41.3Z" }, - { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, - { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, - { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, - { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, - { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, - { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, - { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, - { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" }, - { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, - { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" }, - { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, -] - [[package]] name = "send2trash" version = "1.8.3"