edsl 0.1.39.dev3__py3-none-any.whl → 0.1.39.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/Base.py +413 -332
- edsl/BaseDiff.py +260 -260
- edsl/TemplateLoader.py +24 -24
- edsl/__init__.py +57 -49
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +1071 -867
- edsl/agents/AgentList.py +551 -413
- edsl/agents/Invigilator.py +284 -233
- edsl/agents/InvigilatorBase.py +257 -270
- edsl/agents/PromptConstructor.py +272 -354
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +2 -3
- edsl/agents/descriptors.py +99 -99
- edsl/agents/prompt_helpers.py +129 -129
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +130 -117
- edsl/auto/StageBase.py +243 -230
- edsl/auto/StageGenerateSurvey.py +178 -178
- edsl/auto/StageLabelQuestions.py +125 -125
- edsl/auto/StagePersona.py +61 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
- edsl/auto/StagePersonaDimensionValues.py +74 -74
- edsl/auto/StagePersonaDimensions.py +69 -69
- edsl/auto/StageQuestions.py +74 -73
- edsl/auto/SurveyCreatorPipeline.py +21 -21
- edsl/auto/utilities.py +218 -224
- edsl/base/Base.py +279 -279
- edsl/config.py +177 -157
- edsl/conversation/Conversation.py +290 -290
- edsl/conversation/car_buying.py +59 -58
- edsl/conversation/chips.py +95 -95
- edsl/conversation/mug_negotiation.py +81 -81
- edsl/conversation/next_speaker_utilities.py +93 -93
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +54 -54
- edsl/coop/__init__.py +2 -2
- edsl/coop/coop.py +1106 -1028
- edsl/coop/utils.py +131 -131
- edsl/data/Cache.py +573 -555
- edsl/data/CacheEntry.py +230 -233
- edsl/data/CacheHandler.py +168 -149
- edsl/data/RemoteCacheSync.py +186 -78
- edsl/data/SQLiteDict.py +292 -292
- edsl/data/__init__.py +5 -4
- edsl/data/orm.py +10 -10
- edsl/data_transfer_models.py +74 -73
- edsl/enums.py +202 -175
- edsl/exceptions/BaseException.py +21 -21
- edsl/exceptions/__init__.py +54 -54
- edsl/exceptions/agents.py +54 -42
- edsl/exceptions/cache.py +5 -5
- edsl/exceptions/configuration.py +16 -16
- edsl/exceptions/coop.py +10 -10
- edsl/exceptions/data.py +14 -14
- edsl/exceptions/general.py +34 -34
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/jobs.py +33 -33
- edsl/exceptions/language_models.py +63 -63
- edsl/exceptions/prompts.py +15 -15
- edsl/exceptions/questions.py +109 -91
- edsl/exceptions/results.py +29 -29
- edsl/exceptions/scenarios.py +29 -22
- edsl/exceptions/surveys.py +37 -37
- edsl/inference_services/AnthropicService.py +106 -87
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +118 -120
- edsl/inference_services/AzureAI.py +215 -217
- edsl/inference_services/DeepInfraService.py +18 -18
- edsl/inference_services/GoogleService.py +143 -148
- edsl/inference_services/GroqService.py +20 -20
- edsl/inference_services/InferenceServiceABC.py +80 -147
- edsl/inference_services/InferenceServicesCollection.py +138 -97
- edsl/inference_services/MistralAIService.py +120 -123
- edsl/inference_services/OllamaService.py +18 -18
- edsl/inference_services/OpenAIService.py +236 -224
- edsl/inference_services/PerplexityService.py +160 -163
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +90 -89
- edsl/inference_services/TogetherAIService.py +172 -170
- edsl/inference_services/data_structures.py +134 -0
- edsl/inference_services/models_available_cache.py +118 -118
- edsl/inference_services/rate_limits_cache.py +25 -25
- edsl/inference_services/registry.py +41 -41
- edsl/inference_services/write_available.py +10 -10
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +43 -56
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +823 -898
- edsl/jobs/JobsChecks.py +172 -147
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +270 -268
- edsl/jobs/JobsRemoteInferenceHandler.py +311 -239
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/__init__.py +1 -1
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +104 -63
- edsl/jobs/buckets/ModelBuckets.py +65 -65
- edsl/jobs/buckets/TokenBucket.py +283 -251
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +396 -661
- edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
- edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
- edsl/jobs/interviews/InterviewStatistic.py +63 -63
- edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
- edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
- edsl/jobs/interviews/InterviewStatusLog.py +92 -92
- edsl/jobs/interviews/ReportErrors.py +66 -66
- edsl/jobs/interviews/interview_status_enum.py +9 -9
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +151 -466
- edsl/jobs/runners/JobsRunnerStatus.py +297 -330
- edsl/jobs/tasks/QuestionTaskCreator.py +244 -242
- edsl/jobs/tasks/TaskCreators.py +64 -64
- edsl/jobs/tasks/TaskHistory.py +470 -450
- edsl/jobs/tasks/TaskStatusLog.py +23 -23
- edsl/jobs/tasks/task_status_enum.py +161 -163
- edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
- edsl/jobs/tokens/TokenUsage.py +34 -34
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +626 -668
- edsl/language_models/ModelList.py +164 -155
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +2 -3
- edsl/language_models/fake_openai_call.py +15 -15
- edsl/language_models/fake_openai_service.py +61 -61
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +156 -156
- edsl/language_models/utilities.py +65 -64
- edsl/notebooks/Notebook.py +263 -258
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/notebooks/__init__.py +1 -1
- edsl/prompts/Prompt.py +352 -362
- edsl/prompts/__init__.py +2 -2
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +518 -664
- edsl/questions/QuestionBasePromptsMixin.py +221 -217
- edsl/questions/QuestionBudget.py +227 -227
- edsl/questions/QuestionCheckBox.py +359 -359
- edsl/questions/QuestionExtract.py +180 -182
- edsl/questions/QuestionFreeText.py +113 -114
- edsl/questions/QuestionFunctional.py +166 -166
- edsl/questions/QuestionList.py +223 -231
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +330 -286
- edsl/questions/QuestionNumerical.py +151 -153
- edsl/questions/QuestionRank.py +314 -324
- edsl/questions/Quick.py +41 -41
- edsl/questions/SimpleAskMixin.py +74 -73
- edsl/questions/__init__.py +27 -26
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +334 -289
- edsl/questions/compose_questions.py +98 -98
- edsl/questions/data_structures.py +20 -0
- edsl/questions/decorators.py +21 -21
- edsl/questions/derived/QuestionLikertFive.py +76 -76
- edsl/questions/derived/QuestionLinearScale.py +90 -87
- edsl/questions/derived/QuestionTopK.py +93 -93
- edsl/questions/derived/QuestionYesNo.py +82 -82
- edsl/questions/descriptors.py +427 -413
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/prompt_templates/question_budget.jinja +13 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
- edsl/questions/prompt_templates/question_extract.jinja +11 -11
- edsl/questions/prompt_templates/question_free_text.jinja +3 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
- edsl/questions/prompt_templates/question_list.jinja +17 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
- edsl/questions/prompt_templates/question_numerical.jinja +36 -36
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +168 -161
- edsl/questions/question_registry.py +177 -177
- edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +71 -71
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +188 -174
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/settings.py +12 -12
- edsl/questions/templates/budget/answering_instructions.jinja +7 -7
- edsl/questions/templates/budget/question_presentation.jinja +7 -7
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
- edsl/questions/templates/extract/answering_instructions.jinja +7 -7
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
- edsl/questions/templates/list/answering_instructions.jinja +3 -3
- edsl/questions/templates/list/question_presentation.jinja +5 -5
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
- edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
- edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
- edsl/questions/templates/numerical/question_presentation.jinja +6 -6
- edsl/questions/templates/rank/answering_instructions.jinja +11 -11
- edsl/questions/templates/rank/question_presentation.jinja +15 -15
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
- edsl/questions/templates/top_k/question_presentation.jinja +22 -22
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
- edsl/results/CSSParameterizer.py +108 -108
- edsl/results/Dataset.py +587 -424
- edsl/results/DatasetExportMixin.py +594 -731
- edsl/results/DatasetTree.py +295 -275
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +557 -465
- edsl/results/Results.py +1183 -1165
- edsl/results/ResultsExportMixin.py +45 -43
- edsl/results/ResultsGGMixin.py +121 -121
- edsl/results/TableDisplay.py +125 -198
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +2 -2
- edsl/results/file_exports.py +252 -0
- edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +33 -33
- edsl/results/{Selector.py → results_selector.py} +145 -135
- edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +98 -98
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_display.css +77 -77
- edsl/results/table_renderers.py +118 -0
- edsl/results/tree_explore.py +115 -115
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +511 -632
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +498 -601
- edsl/scenarios/ScenarioHtmlMixin.py +65 -64
- edsl/scenarios/ScenarioList.py +1458 -1287
- edsl/scenarios/ScenarioListExportMixin.py +45 -52
- edsl/scenarios/ScenarioListPdfMixin.py +239 -261
- edsl/scenarios/__init__.py +3 -4
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +38 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +131 -127
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/shared.py +1 -1
- edsl/study/ObjectEntry.py +173 -173
- edsl/study/ProofOfWork.py +113 -113
- edsl/study/SnapShot.py +80 -80
- edsl/study/Study.py +521 -528
- edsl/study/__init__.py +4 -4
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/DAG.py +148 -148
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/Memory.py +31 -31
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/MemoryPlan.py +244 -244
- edsl/surveys/Rule.py +327 -326
- edsl/surveys/RuleCollection.py +385 -387
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +1280 -1801
- edsl/surveys/SurveyCSS.py +273 -261
- edsl/surveys/SurveyExportMixin.py +259 -259
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +181 -179
- edsl/surveys/SurveyQualtricsImport.py +284 -284
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +5 -3
- edsl/surveys/base.py +53 -53
- edsl/surveys/descriptors.py +60 -56
- edsl/surveys/instructions/ChangeInstruction.py +48 -49
- edsl/surveys/instructions/Instruction.py +56 -65
- edsl/surveys/instructions/InstructionCollection.py +82 -77
- edsl/templates/error_reporting/base.html +23 -23
- edsl/templates/error_reporting/exceptions_by_model.html +34 -34
- edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
- edsl/templates/error_reporting/exceptions_by_type.html +16 -16
- edsl/templates/error_reporting/interview_details.html +115 -115
- edsl/templates/error_reporting/interviews.html +19 -19
- edsl/templates/error_reporting/overview.html +4 -4
- edsl/templates/error_reporting/performance_plot.html +1 -1
- edsl/templates/error_reporting/report.css +73 -73
- edsl/templates/error_reporting/report.html +117 -117
- edsl/templates/error_reporting/report.js +25 -25
- edsl/tools/__init__.py +1 -1
- edsl/tools/clusters.py +192 -192
- edsl/tools/embeddings.py +27 -27
- edsl/tools/embeddings_plotting.py +118 -118
- edsl/tools/plotting.py +112 -112
- edsl/tools/summarize.py +18 -18
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/SystemInfo.py +28 -28
- edsl/utilities/__init__.py +22 -22
- edsl/utilities/ast_utilities.py +25 -25
- edsl/utilities/data/Registry.py +6 -6
- edsl/utilities/data/__init__.py +1 -1
- edsl/utilities/data/scooter_results.json +1 -1
- edsl/utilities/decorators.py +77 -77
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
- edsl/utilities/interface.py +627 -627
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/naming_utilities.py +263 -263
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/repair_functions.py +28 -28
- edsl/utilities/restricted_python.py +70 -70
- edsl/utilities/utilities.py +436 -424
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/LICENSE +21 -21
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/METADATA +13 -11
- edsl-0.1.39.dev5.dist-info/RECORD +358 -0
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.39.dev3.dist-info/RECORD +0 -277
edsl/study/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from edsl.study.ObjectEntry import ObjectEntry
|
2
|
-
from edsl.study.ProofOfWork import ProofOfWork
|
3
|
-
from edsl.study.SnapShot import SnapShot
|
4
|
-
from edsl.study.Study import Study
|
1
|
+
from edsl.study.ObjectEntry import ObjectEntry
|
2
|
+
from edsl.study.ProofOfWork import ProofOfWork
|
3
|
+
from edsl.study.SnapShot import SnapShot
|
4
|
+
from edsl.study.Study import Study
|
@@ -0,0 +1,92 @@
|
|
1
|
+
from edsl.surveys.base import EndOfSurvey
|
2
|
+
from edsl.surveys.DAG import DAG
|
3
|
+
from edsl.exceptions.surveys import SurveyError
|
4
|
+
|
5
|
+
|
6
|
+
class ConstructDAG:
|
7
|
+
def __init__(self, survey):
|
8
|
+
self.survey = survey
|
9
|
+
self.questions = survey.questions
|
10
|
+
|
11
|
+
self.parameters_by_question = self.survey.parameters_by_question
|
12
|
+
self.question_name_to_index = self.survey.question_name_to_index
|
13
|
+
|
14
|
+
def dag(self, textify: bool = False) -> DAG:
|
15
|
+
memory_dag = self.survey.memory_plan.dag
|
16
|
+
rule_dag = self.survey.rule_collection.dag
|
17
|
+
piping_dag = self.piping_dag
|
18
|
+
if textify:
|
19
|
+
memory_dag = DAG(self.textify(memory_dag))
|
20
|
+
rule_dag = DAG(self.textify(rule_dag))
|
21
|
+
piping_dag = DAG(self.textify(piping_dag))
|
22
|
+
return memory_dag + rule_dag + piping_dag
|
23
|
+
|
24
|
+
@property
|
25
|
+
def piping_dag(self) -> DAG:
|
26
|
+
"""Figures out the DAG of piping dependencies.
|
27
|
+
|
28
|
+
>>> from edsl import Survey
|
29
|
+
>>> from edsl import QuestionFreeText
|
30
|
+
>>> q0 = QuestionFreeText(question_text="Here is a question", question_name="q0")
|
31
|
+
>>> q1 = QuestionFreeText(question_text="You previously answered {{ q0 }}---how do you feel now?", question_name="q1")
|
32
|
+
>>> s = Survey([q0, q1])
|
33
|
+
>>> ConstructDAG(s).piping_dag
|
34
|
+
{1: {0}}
|
35
|
+
"""
|
36
|
+
d = {}
|
37
|
+
for question_name, depenencies in self.parameters_by_question.items():
|
38
|
+
if depenencies:
|
39
|
+
question_index = self.question_name_to_index[question_name]
|
40
|
+
for dependency in depenencies:
|
41
|
+
if dependency not in self.question_name_to_index:
|
42
|
+
pass
|
43
|
+
else:
|
44
|
+
dependency_index = self.question_name_to_index[dependency]
|
45
|
+
if question_index not in d:
|
46
|
+
d[question_index] = set()
|
47
|
+
d[question_index].add(dependency_index)
|
48
|
+
return d
|
49
|
+
|
50
|
+
def textify(self, index_dag: DAG) -> DAG:
|
51
|
+
"""Convert the DAG of question indices to a DAG of question names.
|
52
|
+
|
53
|
+
:param index_dag: The DAG of question indices.
|
54
|
+
|
55
|
+
Example:
|
56
|
+
|
57
|
+
>>> from edsl import Survey
|
58
|
+
>>> s = Survey.example()
|
59
|
+
>>> d = s.dag()
|
60
|
+
>>> d
|
61
|
+
{1: {0}, 2: {0}}
|
62
|
+
>>> ConstructDAG(s).textify(d)
|
63
|
+
{'q1': {'q0'}, 'q2': {'q0'}}
|
64
|
+
"""
|
65
|
+
|
66
|
+
def get_name(index: int):
|
67
|
+
"""Return the name of the question given the index."""
|
68
|
+
if index >= len(self.questions):
|
69
|
+
return EndOfSurvey
|
70
|
+
try:
|
71
|
+
return self.questions[index].question_name
|
72
|
+
except IndexError:
|
73
|
+
print(
|
74
|
+
f"The index is {index} but the length of the questions is {len(self.questions)}"
|
75
|
+
)
|
76
|
+
raise SurveyError
|
77
|
+
|
78
|
+
try:
|
79
|
+
text_dag = {}
|
80
|
+
for child_index, parent_indices in index_dag.items():
|
81
|
+
parent_names = {get_name(index) for index in parent_indices}
|
82
|
+
child_name = get_name(child_index)
|
83
|
+
text_dag[child_name] = parent_names
|
84
|
+
return text_dag
|
85
|
+
except IndexError:
|
86
|
+
raise
|
87
|
+
|
88
|
+
|
89
|
+
if __name__ == "__main__":
|
90
|
+
import doctest
|
91
|
+
|
92
|
+
doctest.testmod()
|
edsl/surveys/DAG.py
CHANGED
@@ -1,148 +1,148 @@
|
|
1
|
-
"""Directed Acyclic Graph (DAG) class."""
|
2
|
-
|
3
|
-
from collections import UserDict
|
4
|
-
from graphlib import TopologicalSorter
|
5
|
-
|
6
|
-
|
7
|
-
class DAG(UserDict):
|
8
|
-
"""Class for creating a Directed Acyclic Graph (DAG) from a dictionary."""
|
9
|
-
|
10
|
-
def __init__(self, data: dict):
|
11
|
-
"""Initialize the DAG class."""
|
12
|
-
super().__init__(data)
|
13
|
-
self.reverse_mapping = self._create_reverse_mapping()
|
14
|
-
self.validate_no_cycles()
|
15
|
-
|
16
|
-
def _create_reverse_mapping(self):
|
17
|
-
"""
|
18
|
-
Create a reverse mapping of the DAG, where the keys are the children and the values are the parents.
|
19
|
-
|
20
|
-
Example usage:
|
21
|
-
|
22
|
-
.. code-block:: python
|
23
|
-
|
24
|
-
data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
|
25
|
-
dag = DAG(data)
|
26
|
-
dag._create_reverse_mapping()
|
27
|
-
{'b': {'a'}, 'c': {'a'}, 'd': {'b'}}
|
28
|
-
|
29
|
-
"""
|
30
|
-
rev_map = {}
|
31
|
-
for key, values in self.items():
|
32
|
-
for value in values:
|
33
|
-
rev_map.setdefault(value, set()).add(key)
|
34
|
-
return rev_map
|
35
|
-
|
36
|
-
def get_all_children(self, key):
|
37
|
-
"""Get all children of a node in the DAG."""
|
38
|
-
children = set()
|
39
|
-
|
40
|
-
def dfs(node):
|
41
|
-
for child in self.reverse_mapping.get(node, []):
|
42
|
-
if child not in children:
|
43
|
-
children.add(child)
|
44
|
-
dfs(child)
|
45
|
-
|
46
|
-
dfs(key)
|
47
|
-
return children
|
48
|
-
|
49
|
-
def topologically_sorted_nodes(self):
|
50
|
-
"""
|
51
|
-
Return a sequence of the DAG.
|
52
|
-
|
53
|
-
Example usage:
|
54
|
-
|
55
|
-
.. code-block:: python
|
56
|
-
|
57
|
-
data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
|
58
|
-
dag = DAG(data)
|
59
|
-
dag.topologically_sorted_nodes() == ['c', 'd', 'b', 'a']
|
60
|
-
True
|
61
|
-
|
62
|
-
"""
|
63
|
-
return list(TopologicalSorter(self).static_order())
|
64
|
-
|
65
|
-
def __add__(self, other_dag):
|
66
|
-
"""Combine two DAGs."""
|
67
|
-
d = {}
|
68
|
-
combined_keys = set(self.keys()).union(set(other_dag.keys()))
|
69
|
-
for key in combined_keys:
|
70
|
-
d[key] = self.get(key, set({})).union(other_dag.get(key, set({})))
|
71
|
-
return DAG(d)
|
72
|
-
# if textify:
|
73
|
-
# return DAG(self.textify(d))
|
74
|
-
# else:
|
75
|
-
# return DAG(d)
|
76
|
-
|
77
|
-
def remove_node(self, node: int) -> None:
|
78
|
-
"""Remove a node and all its connections from the DAG."""
|
79
|
-
self.pop(node, None)
|
80
|
-
for connections in self.values():
|
81
|
-
connections.discard(node)
|
82
|
-
# Adjust remaining nodes if necessary
|
83
|
-
self._adjust_nodes_after_removal(node)
|
84
|
-
|
85
|
-
def _adjust_nodes_after_removal(self, removed_node: int) -> None:
|
86
|
-
"""Adjust node indices after a node is removed."""
|
87
|
-
new_dag = {}
|
88
|
-
for node, connections in self.items():
|
89
|
-
new_node = node if node < removed_node else node - 1
|
90
|
-
new_connections = {c if c < removed_node else c - 1 for c in connections}
|
91
|
-
new_dag[new_node] = new_connections
|
92
|
-
self.clear()
|
93
|
-
self.update(new_dag)
|
94
|
-
|
95
|
-
@classmethod
|
96
|
-
def example(cls):
|
97
|
-
"""Return an example of the `DAG`."""
|
98
|
-
data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
|
99
|
-
return cls(data)
|
100
|
-
|
101
|
-
def detect_cycles(self):
|
102
|
-
"""
|
103
|
-
Detect cycles in the DAG using depth-first search.
|
104
|
-
|
105
|
-
:return: A list of cycles if any are found, otherwise an empty list.
|
106
|
-
"""
|
107
|
-
visited = set()
|
108
|
-
path = []
|
109
|
-
cycles = []
|
110
|
-
|
111
|
-
def dfs(node):
|
112
|
-
if node in path:
|
113
|
-
cycle = path[path.index(node) :]
|
114
|
-
cycles.append(cycle + [node])
|
115
|
-
return
|
116
|
-
|
117
|
-
if node in visited:
|
118
|
-
return
|
119
|
-
|
120
|
-
visited.add(node)
|
121
|
-
path.append(node)
|
122
|
-
|
123
|
-
for child in self.get(node, []):
|
124
|
-
dfs(child)
|
125
|
-
|
126
|
-
path.pop()
|
127
|
-
|
128
|
-
for node in self:
|
129
|
-
if node not in visited:
|
130
|
-
dfs(node)
|
131
|
-
|
132
|
-
return cycles
|
133
|
-
|
134
|
-
def validate_no_cycles(self):
|
135
|
-
"""
|
136
|
-
Validate that the DAG does not contain any cycles.
|
137
|
-
|
138
|
-
:raises ValueError: If cycles are detected in the DAG.
|
139
|
-
"""
|
140
|
-
cycles = self.detect_cycles()
|
141
|
-
if cycles:
|
142
|
-
raise ValueError(f"Cycles detected in the DAG: {cycles}")
|
143
|
-
|
144
|
-
|
145
|
-
if __name__ == "__main__":
|
146
|
-
import doctest
|
147
|
-
|
148
|
-
doctest.testmod()
|
1
|
+
"""Directed Acyclic Graph (DAG) class."""
|
2
|
+
|
3
|
+
from collections import UserDict
|
4
|
+
from graphlib import TopologicalSorter
|
5
|
+
|
6
|
+
|
7
|
+
class DAG(UserDict):
|
8
|
+
"""Class for creating a Directed Acyclic Graph (DAG) from a dictionary."""
|
9
|
+
|
10
|
+
def __init__(self, data: dict):
|
11
|
+
"""Initialize the DAG class."""
|
12
|
+
super().__init__(data)
|
13
|
+
self.reverse_mapping = self._create_reverse_mapping()
|
14
|
+
self.validate_no_cycles()
|
15
|
+
|
16
|
+
def _create_reverse_mapping(self):
|
17
|
+
"""
|
18
|
+
Create a reverse mapping of the DAG, where the keys are the children and the values are the parents.
|
19
|
+
|
20
|
+
Example usage:
|
21
|
+
|
22
|
+
.. code-block:: python
|
23
|
+
|
24
|
+
data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
|
25
|
+
dag = DAG(data)
|
26
|
+
dag._create_reverse_mapping()
|
27
|
+
{'b': {'a'}, 'c': {'a'}, 'd': {'b'}}
|
28
|
+
|
29
|
+
"""
|
30
|
+
rev_map = {}
|
31
|
+
for key, values in self.items():
|
32
|
+
for value in values:
|
33
|
+
rev_map.setdefault(value, set()).add(key)
|
34
|
+
return rev_map
|
35
|
+
|
36
|
+
def get_all_children(self, key):
|
37
|
+
"""Get all children of a node in the DAG."""
|
38
|
+
children = set()
|
39
|
+
|
40
|
+
def dfs(node):
|
41
|
+
for child in self.reverse_mapping.get(node, []):
|
42
|
+
if child not in children:
|
43
|
+
children.add(child)
|
44
|
+
dfs(child)
|
45
|
+
|
46
|
+
dfs(key)
|
47
|
+
return children
|
48
|
+
|
49
|
+
def topologically_sorted_nodes(self):
|
50
|
+
"""
|
51
|
+
Return a sequence of the DAG.
|
52
|
+
|
53
|
+
Example usage:
|
54
|
+
|
55
|
+
.. code-block:: python
|
56
|
+
|
57
|
+
data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
|
58
|
+
dag = DAG(data)
|
59
|
+
dag.topologically_sorted_nodes() == ['c', 'd', 'b', 'a']
|
60
|
+
True
|
61
|
+
|
62
|
+
"""
|
63
|
+
return list(TopologicalSorter(self).static_order())
|
64
|
+
|
65
|
+
def __add__(self, other_dag):
|
66
|
+
"""Combine two DAGs."""
|
67
|
+
d = {}
|
68
|
+
combined_keys = set(self.keys()).union(set(other_dag.keys()))
|
69
|
+
for key in combined_keys:
|
70
|
+
d[key] = self.get(key, set({})).union(other_dag.get(key, set({})))
|
71
|
+
return DAG(d)
|
72
|
+
# if textify:
|
73
|
+
# return DAG(self.textify(d))
|
74
|
+
# else:
|
75
|
+
# return DAG(d)
|
76
|
+
|
77
|
+
def remove_node(self, node: int) -> None:
|
78
|
+
"""Remove a node and all its connections from the DAG."""
|
79
|
+
self.pop(node, None)
|
80
|
+
for connections in self.values():
|
81
|
+
connections.discard(node)
|
82
|
+
# Adjust remaining nodes if necessary
|
83
|
+
self._adjust_nodes_after_removal(node)
|
84
|
+
|
85
|
+
def _adjust_nodes_after_removal(self, removed_node: int) -> None:
|
86
|
+
"""Adjust node indices after a node is removed."""
|
87
|
+
new_dag = {}
|
88
|
+
for node, connections in self.items():
|
89
|
+
new_node = node if node < removed_node else node - 1
|
90
|
+
new_connections = {c if c < removed_node else c - 1 for c in connections}
|
91
|
+
new_dag[new_node] = new_connections
|
92
|
+
self.clear()
|
93
|
+
self.update(new_dag)
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def example(cls):
|
97
|
+
"""Return an example of the `DAG`."""
|
98
|
+
data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
|
99
|
+
return cls(data)
|
100
|
+
|
101
|
+
def detect_cycles(self):
|
102
|
+
"""
|
103
|
+
Detect cycles in the DAG using depth-first search.
|
104
|
+
|
105
|
+
:return: A list of cycles if any are found, otherwise an empty list.
|
106
|
+
"""
|
107
|
+
visited = set()
|
108
|
+
path = []
|
109
|
+
cycles = []
|
110
|
+
|
111
|
+
def dfs(node):
|
112
|
+
if node in path:
|
113
|
+
cycle = path[path.index(node) :]
|
114
|
+
cycles.append(cycle + [node])
|
115
|
+
return
|
116
|
+
|
117
|
+
if node in visited:
|
118
|
+
return
|
119
|
+
|
120
|
+
visited.add(node)
|
121
|
+
path.append(node)
|
122
|
+
|
123
|
+
for child in self.get(node, []):
|
124
|
+
dfs(child)
|
125
|
+
|
126
|
+
path.pop()
|
127
|
+
|
128
|
+
for node in self:
|
129
|
+
if node not in visited:
|
130
|
+
dfs(node)
|
131
|
+
|
132
|
+
return cycles
|
133
|
+
|
134
|
+
def validate_no_cycles(self):
|
135
|
+
"""
|
136
|
+
Validate that the DAG does not contain any cycles.
|
137
|
+
|
138
|
+
:raises ValueError: If cycles are detected in the DAG.
|
139
|
+
"""
|
140
|
+
cycles = self.detect_cycles()
|
141
|
+
if cycles:
|
142
|
+
raise ValueError(f"Cycles detected in the DAG: {cycles}")
|
143
|
+
|
144
|
+
|
145
|
+
if __name__ == "__main__":
|
146
|
+
import doctest
|
147
|
+
|
148
|
+
doctest.testmod()
|
@@ -0,0 +1,221 @@
|
|
1
|
+
from typing import Union, Optional, TYPE_CHECKING
|
2
|
+
from edsl.exceptions.surveys import SurveyError
|
3
|
+
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from edsl.questions.QuestionBase import QuestionBase
|
6
|
+
|
7
|
+
from edsl.exceptions.surveys import SurveyError, SurveyCreationError
|
8
|
+
from edsl.surveys.Rule import Rule
|
9
|
+
from edsl.surveys.base import RulePriority, EndOfSurvey
|
10
|
+
|
11
|
+
|
12
|
+
class EditSurvey:
|
13
|
+
def __init__(self, survey):
|
14
|
+
self.survey = survey
|
15
|
+
|
16
|
+
def move_question(self, identifier: Union[str, int], new_index: int) -> "Survey":
|
17
|
+
if isinstance(identifier, str):
|
18
|
+
if identifier not in self.survey.question_names:
|
19
|
+
raise SurveyError(
|
20
|
+
f"Question name '{identifier}' does not exist in the survey."
|
21
|
+
)
|
22
|
+
index = self.survey.question_name_to_index[identifier]
|
23
|
+
elif isinstance(identifier, int):
|
24
|
+
if identifier < 0 or identifier >= len(self.survey.questions):
|
25
|
+
raise SurveyError(f"Index {identifier} is out of range.")
|
26
|
+
index = identifier
|
27
|
+
else:
|
28
|
+
raise SurveyError(
|
29
|
+
"Identifier must be either a string (question name) or an integer (question index)."
|
30
|
+
)
|
31
|
+
|
32
|
+
moving_question = self.survey._questions[index]
|
33
|
+
|
34
|
+
new_survey = self.survey.delete_question(index)
|
35
|
+
new_survey.add_question(moving_question, new_index)
|
36
|
+
return new_survey
|
37
|
+
|
38
|
+
def add_question(
|
39
|
+
self, question: "QuestionBase", index: Optional[int] = None
|
40
|
+
) -> "Survey":
|
41
|
+
if question.question_name in self.survey.question_names:
|
42
|
+
raise SurveyCreationError(
|
43
|
+
f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.survey.question_names}."""
|
44
|
+
)
|
45
|
+
if index is None:
|
46
|
+
index = len(self.survey.questions)
|
47
|
+
|
48
|
+
if index > len(self.survey.questions):
|
49
|
+
raise SurveyCreationError(
|
50
|
+
f"Index {index} is greater than the number of questions in the survey."
|
51
|
+
)
|
52
|
+
if index < 0:
|
53
|
+
raise SurveyCreationError(f"Index {index} is less than 0.")
|
54
|
+
|
55
|
+
interior_insertion = index != len(self.survey.questions)
|
56
|
+
|
57
|
+
# index = len(self.survey.questions)
|
58
|
+
# TODO: This is a bit ugly because the user
|
59
|
+
# doesn't "know" about _questions - it's generated by the
|
60
|
+
# descriptor.
|
61
|
+
self.survey._questions.insert(index, question)
|
62
|
+
|
63
|
+
if interior_insertion:
|
64
|
+
for question_name, old_index in self.survey._pseudo_indices.items():
|
65
|
+
if old_index >= index:
|
66
|
+
self.survey._pseudo_indices[question_name] = old_index + 1
|
67
|
+
|
68
|
+
self.survey._pseudo_indices[question.question_name] = index
|
69
|
+
|
70
|
+
## Re-do question_name to index - this is done automatically
|
71
|
+
# for question_name, old_index in self.survey.question_name_to_index.items():
|
72
|
+
# if old_index >= index:
|
73
|
+
# self.survey.question_name_to_index[question_name] = old_index + 1
|
74
|
+
|
75
|
+
## Need to re-do the rule collection and the indices of the questions
|
76
|
+
|
77
|
+
## If a rule is before the insertion index and next_q is also before the insertion index, no change needed.
|
78
|
+
## If the rule is before the insertion index but next_q is after the insertion index, increment the next_q by 1
|
79
|
+
## If the rule is after the insertion index, increment the current_q by 1 and the next_q by 1
|
80
|
+
|
81
|
+
# using index + 1 presumes there is a next question
|
82
|
+
if interior_insertion:
|
83
|
+
for rule in self.survey.rule_collection:
|
84
|
+
if rule.current_q >= index:
|
85
|
+
rule.current_q += 1
|
86
|
+
if rule.next_q >= index:
|
87
|
+
rule.next_q += 1
|
88
|
+
|
89
|
+
# add a new rule
|
90
|
+
self.survey.rule_collection.add_rule(
|
91
|
+
Rule(
|
92
|
+
current_q=index,
|
93
|
+
expression="True",
|
94
|
+
next_q=index + 1,
|
95
|
+
question_name_to_index=self.survey.question_name_to_index,
|
96
|
+
priority=RulePriority.DEFAULT.value,
|
97
|
+
)
|
98
|
+
)
|
99
|
+
|
100
|
+
# a question might be added before the memory plan is created
|
101
|
+
# it's ok because the memory plan will be updated when it is created
|
102
|
+
if hasattr(self.survey, "memory_plan"):
|
103
|
+
self.survey.memory_plan.add_question(question)
|
104
|
+
|
105
|
+
return self.survey
|
106
|
+
|
107
|
+
def delete_question(self, identifier: Union[str, int]) -> "Survey":
|
108
|
+
"""
|
109
|
+
Delete a question from the survey.
|
110
|
+
|
111
|
+
:param identifier: The name or index of the question to delete.
|
112
|
+
:return: The updated Survey object.
|
113
|
+
|
114
|
+
>>> from edsl import QuestionMultipleChoice, Survey
|
115
|
+
>>> q1 = QuestionMultipleChoice(question_text="Q1", question_options=["A", "B"], question_name="q1")
|
116
|
+
>>> q2 = QuestionMultipleChoice(question_text="Q2", question_options=["C", "D"], question_name="q2")
|
117
|
+
>>> s = Survey().add_question(q1).add_question(q2)
|
118
|
+
>>> _ = s.delete_question("q1")
|
119
|
+
>>> len(s.questions)
|
120
|
+
1
|
121
|
+
>>> _ = s.delete_question(0)
|
122
|
+
>>> len(s.questions)
|
123
|
+
0
|
124
|
+
"""
|
125
|
+
if isinstance(identifier, str):
|
126
|
+
if identifier not in self.survey.question_names:
|
127
|
+
raise SurveyError(
|
128
|
+
f"Question name '{identifier}' does not exist in the survey."
|
129
|
+
)
|
130
|
+
index = self.survey.question_name_to_index[identifier]
|
131
|
+
elif isinstance(identifier, int):
|
132
|
+
if identifier < 0 or identifier >= len(self.survey.questions):
|
133
|
+
raise SurveyError(f"Index {identifier} is out of range.")
|
134
|
+
index = identifier
|
135
|
+
else:
|
136
|
+
raise SurveyError(
|
137
|
+
"Identifier must be either a string (question name) or an integer (question index)."
|
138
|
+
)
|
139
|
+
|
140
|
+
# Remove the question
|
141
|
+
deleted_question = self.survey._questions.pop(index)
|
142
|
+
del self.survey._pseudo_indices[deleted_question.question_name]
|
143
|
+
|
144
|
+
# Update indices
|
145
|
+
for question_name, old_index in self.survey._pseudo_indices.items():
|
146
|
+
if old_index > index:
|
147
|
+
self.survey._pseudo_indices[question_name] = old_index - 1
|
148
|
+
|
149
|
+
# Update rules
|
150
|
+
from .RuleCollection import RuleCollection
|
151
|
+
|
152
|
+
new_rule_collection = RuleCollection()
|
153
|
+
for rule in self.survey.rule_collection:
|
154
|
+
if rule.current_q == index:
|
155
|
+
continue # Remove rules associated with the deleted question
|
156
|
+
if rule.current_q > index:
|
157
|
+
rule.current_q -= 1
|
158
|
+
if rule.next_q > index:
|
159
|
+
rule.next_q -= 1
|
160
|
+
|
161
|
+
if rule.next_q == index:
|
162
|
+
if index == len(self.survey.questions):
|
163
|
+
rule.next_q = EndOfSurvey
|
164
|
+
else:
|
165
|
+
rule.next_q = index
|
166
|
+
|
167
|
+
new_rule_collection.add_rule(rule)
|
168
|
+
self.survey.rule_collection = new_rule_collection
|
169
|
+
|
170
|
+
# Update memory plan if it exists
|
171
|
+
if hasattr(self.survey, "memory_plan"):
|
172
|
+
self.survey.memory_plan.remove_question(deleted_question.question_name)
|
173
|
+
|
174
|
+
return self.survey
|
175
|
+
|
176
|
+
def add_instruction(
|
177
|
+
self, instruction: Union["Instruction", "ChangeInstruction"]
|
178
|
+
) -> "Survey":
|
179
|
+
"""
|
180
|
+
Add an instruction to the survey.
|
181
|
+
|
182
|
+
:param instruction: The instruction to add to the survey.
|
183
|
+
|
184
|
+
>>> from edsl import Instruction
|
185
|
+
>>> from edsl.surveys.Survey import Survey
|
186
|
+
>>> i = Instruction(text="Pay attention to the following questions.", name="intro")
|
187
|
+
>>> s = Survey().add_instruction(i)
|
188
|
+
>>> s._instruction_names_to_instructions
|
189
|
+
{'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
|
190
|
+
>>> s._pseudo_indices
|
191
|
+
{'intro': -0.5}
|
192
|
+
"""
|
193
|
+
import math
|
194
|
+
|
195
|
+
if instruction.name in self.survey._instruction_names_to_instructions:
|
196
|
+
raise SurveyCreationError(
|
197
|
+
f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.survey._instruction_names_to_instructions.keys()}."""
|
198
|
+
)
|
199
|
+
self.survey._instruction_names_to_instructions[instruction.name] = instruction
|
200
|
+
|
201
|
+
# was the last thing added an instruction or a question?
|
202
|
+
if self.survey._pseudo_indices.last_item_was_instruction:
|
203
|
+
pseudo_index = (
|
204
|
+
self.survey._pseudo_indices.max_pseudo_index
|
205
|
+
+ (
|
206
|
+
math.ceil(self.survey._pseudo_indices.max_pseudo_index)
|
207
|
+
- self.survey._pseudo_indices.max_pseudo_index
|
208
|
+
)
|
209
|
+
/ 2
|
210
|
+
)
|
211
|
+
else:
|
212
|
+
pseudo_index = self.survey._pseudo_indices.max_pseudo_index + 1.0 / 2.0
|
213
|
+
self.survey._pseudo_indices[instruction.name] = pseudo_index
|
214
|
+
|
215
|
+
return self.survey
|
216
|
+
|
217
|
+
|
218
|
+
if __name__ == "__main__":
|
219
|
+
import doctest
|
220
|
+
|
221
|
+
doctest.testmod()
|