From 2eda3ec0ab801a79926b00f2bf2c5daa78ba365d Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 08:26:35 -0700 Subject: [PATCH 01/19] Start adding API points for generating multiverse script --- tisane/code_generator.py | 74 ++++++++++----------- tisane/main.py | 136 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 41 deletions(-) diff --git a/tisane/code_generator.py b/tisane/code_generator.py index 5033b8b..6628b38 100644 --- a/tisane/code_generator.py +++ b/tisane/code_generator.py @@ -208,7 +208,7 @@ def generate_python_code(statistical_model: StatisticalModel): return generate_statsmodels_code(statistical_model=statistical_model) -def generate_pymer4_code(statistical_model: StatisticalModel): +def generate_pymer4_code(statistical_model: StatisticalModel, boba_template: bool = False): global pymer4_code_templates ### Specify preamble @@ -260,15 +260,22 @@ def generate_pymer4_code(statistical_model: StatisticalModel): ) -def generate_pymer4_model(statistical_model: StatisticalModel): +def generate_pymer4_model(statistical_model: StatisticalModel, boba_template: bool =False): global pymer4_code_templates - formula_code = generate_pymer4_formula(statistical_model=statistical_model) - family_code = generate_pymer4_family(statistical_model=statistical_model) - # link_code = generate_pymer4_link(statistical_model=statistical_model) - model_code = pymer4_code_templates["model_template"].format( - formula=formula_code, family_name=family_code - ) + if boba_template: + formula_code = "{{FORMULA}}" + family_code = "{{FAMILY}}" + model_code = pymer4_code_templates["model_template"].format( + formula=formula_code, family_name=family_code + ) + else: + formula_code = generate_pymer4_formula(statistical_model=statistical_model) + family_code = generate_pymer4_family(statistical_model=statistical_model) + # link_code = generate_pymer4_link(statistical_model=statistical_model) + model_code = pymer4_code_templates["model_template"].format( + formula=formula_code, family_name=family_code + ) return model_code @@ -361,7 +368,7 @@ def generate_pymer4_family(statistical_model: StatisticalModel) -> str: # return str() -def generate_statsmodels_code(statistical_model: StatisticalModel): +def generate_statsmodels_code(statistical_model: StatisticalModel, boba_template: bool = False): global statsmodels_code_templates ### Specify preamble @@ -384,12 +391,7 @@ def generate_statsmodels_code(statistical_model: StatisticalModel): ].format(path=data_path) ### Generate model code - formula_code = generate_statsmodels_formula(statistical_model=statistical_model) - family_code = generate_statsmodels_family(statistical_model=statistical_model) - link_code = generate_statsmodels_link(statistical_model=statistical_model) - model_code = statsmodels_code_templates["model_template"].format( - formula=formula_code, family_name=family_code, link_obj=link_code - ) + model_code = generate_statsmodels_model(statistical_model=statistical_model, boba_template=boba_template) model_diagnostics_code = statsmodels_code_templates["model_diagnostics"] ### Put everything together @@ -416,18 +418,26 @@ def generate_statsmodels_code(statistical_model: StatisticalModel): ) -def generate_statsmodels_model(statistical_model: StatisticalModel): +def generate_statsmodels_model(statistical_model: StatisticalModel, boba_template: bool = False): global statsmodels_code_templates - formula_code = generate_statsmodels_formula(statistical_model=statistical_model) - family_code = generate_statsmodels_family(statistical_model=statistical_model) - link_code = generate_statsmodels_link(statistical_model=statistical_model) - model_code = statsmodels_code_templates["model_template"].format( - formula=formula_code, family_name=family_code, link_obj=link_code - ) + if boba_template: + formula_code = "{{FORMULA}}" + family_code = "{{FAMILY}}" + link_code = "{{LINK}}" + model_code = statsmodels_code_templates["model_template"].format( + formula=formula_code, family_name=family_code, link_obj=link_code + ) + else: + formula_code = generate_statsmodels_formula(statistical_model=statistical_model) + family_code = generate_statsmodels_family(statistical_model=statistical_model) + link_code = generate_statsmodels_link(statistical_model=statistical_model) + model_code = statsmodels_code_templates["model_template"].format( + formula=formula_code, family_name=family_code, link_obj=link_code + ) - return model_code + return model_code def generate_statsmodels_formula(statistical_model: StatisticalModel): dv_code = "{dv} ~ " @@ -713,20 +723,4 @@ def generate_statsmodels_glmm_code(statistical_model: StatisticalModel, **kwargs + ")" ) - return model_code - - -# def generate_statsmodels_model_code(statistical_model: StatisticalModel, **kwargs): -# model_code = str() - -# has_fixed = len(statistical_model.fixed_ivs) > 0 -# has_interactions = len(statistical_model.interactions) > 0 -# has_random = len(statistical_model.random_ivs) > 0 -# has_data = statistical_model.data is not None # May not have data - -# # Does the statistical model have random effects (slope or intercept) that we should take into consideration? -# if has_random: -# return generate_statsmodels_glmm_code(statistical_model=statistical_model) -# else: -# # GLM: Fixed, interactions, no random; Other family -# return generate_statsmodels_glm_code(statistical_model=statistical_model) + return model_code \ No newline at end of file diff --git a/tisane/main.py b/tisane/main.py index c07b475..bc71be4 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -333,7 +333,6 @@ def infer_model(design: Design, jupyter: bool = False): return infer_statistical_model_from_design(design=design, jupyter=jupyter) # @returns statistical model that reflects the study design - def infer_statistical_model_from_design(design: Design, jupyter: bool = False): """Infer a stats model from design and launch the Tisane GUI. @@ -485,3 +484,138 @@ def generateCode( return path gui.start_app(input=path, jupyter=jupyter, generateCode=generateCode) + +def infer_all_models(design: Design, jupyter: bool = False): + gr = design.graph + + ### Step 1: Initial conceptual checks + # Check that the IVs have a conceptual relationship (direct or transitive) with DV + check_design_ivs(design) + # Check that the DV does not cause one or more IVs + check_design_dv(design) + + ### Step 2: Candidate statistical model inference/generation + (main_effects_candidates, main_explanations) = infer_main_effects_with_explanations( + gr=gr, query=design + ) + ( + interaction_effects_candidates, + interaction_explanations, + ) = infer_interaction_effects_with_explanations( + gr=gr, query=design, main_effects=main_effects_candidates + ) + ( + random_effects_candidates, + random_explanations, + ) = infer_random_effects_with_explanations( + gr=gr, + query=design, + main_effects=main_effects_candidates, + interaction_effects=interaction_effects_candidates, + ) + family_candidates = infer_family_functions(query=design) + link_candidates = set() + family_link_paired = dict() + for f in family_candidates: + l = infer_link_functions(query=design, family=f) + # Add Family: Link options + assert f not in family_link_paired.keys() + family_link_paired[f] = l + + ( + intermediaries, + variable_to_intermediaries, + ) = find_all_associates_that_causes_or_associates_another(design.ivs, design.dv, gr) + intermediary_to_variable = { + intermediary: gr.get_variable(intermediary) for intermediary in intermediaries + } + associative_intermediaries = [ + intermediary + for intermediary, intermediary_variable in intermediary_to_variable.items() + if is_intermediary_associative(intermediary_variable, design.dv, gr) + ] + # Combine explanations + explanations = dict() + explanations.update(main_explanations) + explanations.update(interaction_explanations) + explanations.update(random_explanations) + + # Get combined dict + combined_dict = collect_model_candidates( + query=design, + main_effects_candidates=main_effects_candidates, + interaction_effects_candidates=interaction_effects_candidates, + random_effects_candidates=random_effects_candidates, + family_link_paired_candidates=family_link_paired, + ) + + # Add explanations + combined_dict["input"]["explanations"] = explanations + combined_dict["input"][ + "associative intermediary main effects" + ] = associative_intermediaries + + # Add data + data = design.get_data() + if data is not None: + combined_dict["input"]["data"] = data.to_dict("list") + else: # There is no data + combined_dict["input"]["data"] = dict() + + # Write out to JSON in order to pass data to Tisane GUI for disambiguation + input_file = "input.json" + + # Note: Because the input to the GUI is a JSON file, everything is + # stringified. This means that we need to match up the variable names with + # the actual variable objects in the next step. + # write_to_json returns the Path of the input.json file + path = write_to_json(combined_dict, "./", input_file) + + ### Step 3: Output all the possible models + construct_all_statistical_models(model_options=combined_dict) + + # Output the combined dict to JSON + # Iterate through the JSON, and for each call construct_statistical_model() (as it is currenty implemented) + + # Maybe output all the combinations into separate JSONs, all different files. --> call generateCode for each + + ### Step 3: Disambiguation loop (GUI) + gui = TisaneGUI() + + ### Step 4: GUI generates code + def generateCode( + destinationDir: str = None, modelSpecJson: str = "model_spec.json" + ): + destinationDir = destinationDir or os.getcwd() + output_filename = os.path.join( + destinationDir, modelSpecJson + ) # or whatever path/file that the GUI outputs + + ### Step 4: Code generation + # Construct StatisticalModel from JSON spec + # model_json = f.read() + sm = construct_statistical_model( + filename=output_filename, + query=design, + main_effects_candidates=main_effects_candidates, + interaction_effects_candidates=interaction_effects_candidates, + random_effects_candidates=random_effects_candidates, + family_link_paired_candidates=family_link_paired, + ) + + if design.has_data(): + # Assign statistical model data from @parm design + sm.assign_data(design.dataset) + # Generate code from SM + code = generate_code(sm) + # Write generated code out + + path = write_to_script(code, destinationDir, "model.py") + return path + + # gui.start_app(input=path, jupyter=jupyter, generateCode=generateCode) + + +# TODO: Should this output a Boba script? +def infer_multiverse(): + pass \ No newline at end of file From 788e750ce2c3c732f5c1230d6fd39fc0098af205 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 08:37:02 -0700 Subject: [PATCH 02/19] Replcae with fixes in main --- tisane/variable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tisane/variable.py b/tisane/variable.py index e1a31e0..a828983 100644 --- a/tisane/variable.py +++ b/tisane/variable.py @@ -1247,7 +1247,7 @@ def per(self, cardinality: AbstractVariable=None, number_of_instances: AbstractV ... number_of_instances=Exactly(3).per(cardinality=days)) """ - super(Exactly, self).per(cardinality=cardinality, number_of_instances=number_of_instances) + return super(Exactly, self).per(cardinality=cardinality, number_of_instances=number_of_instances) class AtMost(NumberValue): @@ -1315,7 +1315,7 @@ def per(self, cardinality: AbstractVariable=None, number_of_instances: AbstractV """ - super(AtMost, self).per(cardinality=cardinality, number_of_instances=number_of_instances) + return super(AtMost, self).per(cardinality=cardinality, number_of_instances=number_of_instances) """ Class for expressing Per relationships From 3eba48ebbf393512a47ca18f3618e0bcebf2dde7 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 08:40:29 -0700 Subject: [PATCH 03/19] Replace CI github action with the one from main --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a51c03..08d8670 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,8 +55,7 @@ jobs: - name: Run tests run: | source .venv/bin/activate - pytest tests/ - pytest tests/ --ignore-glob='test_graph_vis.py' .--cov=./ --cov-report=xml + pytest tests/ --ignore=tests/test_graph_vis.py --cov-report=xml - name: Upload coverage data to coveralls.io run: | python -m pip install coveralls==2.2 From a9f3cfb2b8e2d3102e9e357685dae7430f1ea214 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 09:28:40 -0700 Subject: [PATCH 04/19] Reorganize code and ideas for generating multiverse --- tisane/__init__.py | 1 + tisane/data.py | 8 +++++ tisane/main.py | 78 +++++++++++++--------------------------------- 3 files changed, 31 insertions(+), 56 deletions(-) diff --git a/tisane/__init__.py b/tisane/__init__.py index 5d10e7a..43e093d 100644 --- a/tisane/__init__.py +++ b/tisane/__init__.py @@ -1,6 +1,7 @@ from tisane.main import ( infer_model, infer_statistical_model_from_design, + infer_all_models ) from tisane.variable import Unit, SetUp, Exactly, AtMost diff --git a/tisane/data.py b/tisane/data.py index b431912..67619df 100644 --- a/tisane/data.py +++ b/tisane/data.py @@ -63,6 +63,14 @@ def has_data_path(self) -> bool: return self.data_path is not None + # # Outputs data to a CSV file + # # @returns the CSV file's path + # def to_csv(self): + # output_path = "data.csv" + # self.dataset.to_csv(output_path) + + # reutrn output_path + class DataVector(object): name: str diff --git a/tisane/main.py b/tisane/main.py index bc71be4..a3e2c5c 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -485,7 +485,16 @@ def generateCode( gui.start_app(input=path, jupyter=jupyter, generateCode=generateCode) +# Infer a multiverse from the specification +def infer_multiverse(design: Design, jupyter: bool = False): + return infer_all_models(design=design, jupyter=jupyter) + +# Infer a multiverse from the specification def infer_all_models(design: Design, jupyter: bool = False): + return infer_all_statistical_models_from_design(design=design, jupyter=jupyter) + +# Infer a multiverse from the specification +def infer_all_statistical_models_from_design(design: Design, jupyter: bool = False): gr = design.graph ### Step 1: Initial conceptual checks @@ -555,66 +564,23 @@ def infer_all_models(design: Design, jupyter: bool = False): "associative intermediary main effects" ] = associative_intermediaries - # Add data - data = design.get_data() - if data is not None: - combined_dict["input"]["data"] = data.to_dict("list") - else: # There is no data - combined_dict["input"]["data"] = dict() - - # Write out to JSON in order to pass data to Tisane GUI for disambiguation - input_file = "input.json" - - # Note: Because the input to the GUI is a JSON file, everything is - # stringified. This means that we need to match up the variable names with - # the actual variable objects in the next step. - # write_to_json returns the Path of the input.json file - path = write_to_json(combined_dict, "./", input_file) - - ### Step 3: Output all the possible models - construct_all_statistical_models(model_options=combined_dict) - - # Output the combined dict to JSON - # Iterate through the JSON, and for each call construct_statistical_model() (as it is currenty implemented) - - # Maybe output all the combinations into separate JSONs, all different files. --> call generateCode for each - ### Step 3: Disambiguation loop (GUI) - gui = TisaneGUI() + ### Step 3: Generate multiverse code + # Generate the dicitonary representing the multiverse + decisions_file = "decisions.json" + construct_multiverse_decisions(combined_dict, decisions_file) - ### Step 4: GUI generates code - def generateCode( - destinationDir: str = None, modelSpecJson: str = "model_spec.json" - ): - destinationDir = destinationDir or os.getcwd() - output_filename = os.path.join( - destinationDir, modelSpecJson - ) # or whatever path/file that the GUI outputs - - ### Step 4: Code generation - # Construct StatisticalModel from JSON spec - # model_json = f.read() - sm = construct_statistical_model( - filename=output_filename, - query=design, - main_effects_candidates=main_effects_candidates, - interaction_effects_candidates=interaction_effects_candidates, - random_effects_candidates=random_effects_candidates, - family_link_paired_candidates=family_link_paired, - ) - - if design.has_data(): - # Assign statistical model data from @parm design - sm.assign_data(design.dataset) - # Generate code from SM - code = generate_code(sm) - # Write generated code out - - path = write_to_script(code, destinationDir, "model.py") - return path + # Output data somewhere to read in from template.py + data = design.get_data() + data_file = "data.csv" + data.to_csv(data_file) - # gui.start_app(input=path, jupyter=jupyter, generateCode=generateCode) + # Generate template file for the multiverse + # There is only one template file generated because Tisane enforces the inclusion of mixed effects if they are inferred. + template_file = "template.py" + generate_template_code(template_file, decisions_file, data_file) # Need to inject decisions into template file to use boba + ### TODO: Step 4: Generate bash script/output for how to execute Boba? # TODO: Should this output a Boba script? def infer_multiverse(): From ef66cb056ac008b461bba20a8322149010b76b93 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 09:29:20 -0700 Subject: [PATCH 05/19] Add stubs for test cases --- tests/test_generate_multiverse_code.py | 64 ++++++++++++++++++++++++++ tests/test_main.py | 56 ++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/test_generate_multiverse_code.py create mode 100644 tests/test_main.py diff --git a/tests/test_generate_multiverse_code.py b/tests/test_generate_multiverse_code.py new file mode 100644 index 0000000..1f89822 --- /dev/null +++ b/tests/test_generate_multiverse_code.py @@ -0,0 +1,64 @@ +""" +Tests methods called to generate multiverse code +""" +from tisane.family import AbstractFamily, AbstractLink +from tisane.main import ( + check_design_ivs, + check_design_dv, + construct_statistical_model, + infer_family_functions, + infer_link_functions, + infer_main_effects_with_explanations, + infer_interaction_effects_with_explanations, + infer_random_effects_with_explanations, +) +from tisane.code_generator import ( + generate_statsmodels_formula, + generate_statsmodels_family, + generate_statsmodels_link, + generate_statsmodels_model, + generate_pymer4_formula, +) +import tisane as ts +import pandas as pd +from typing import Dict, Set +from pathlib import Path +import os +import unittest + +test_data_repo_name = "output_json_files/" +test_script_repo_name = "output_scripts/" +dir = os.path.dirname(__file__) +data_dir = os.path.join(dir, test_data_repo_name) +script_dir = os.path.join(dir, test_script_repo_name) + +### HELPERS to reduce redundancy across test cases +model_template = """ + model = smf.glm(formula={formula}, data=df, family=sm.families.{family_name}(sm.families.links.{link_obj})) + res = model.fit() + print(res.summary()) + return model +""" + + +def absolute_path(p: str) -> str: + return os.path.join(os.path.dirname(os.path.abspath(__file__)), p) + + +def get_family_link_paired_candidates( + design: ts.Design, +) -> Dict[AbstractFamily, Set[AbstractLink]]: + family_candidates = infer_family_functions(query=design) + family_link_paired = dict() + for f in family_candidates: + l = infer_link_functions(query=design, family=f) + # Add Family: Link options + assert f not in family_link_paired.keys() + family_link_paired[f] = l + + return family_link_paired + + +class GenerateMultiverseCodeHelpersTest(unittest.TestCase): + def test_generate_statsmodels_formula_template(self): + u0 = ts.Unit("Unit") \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..23c570c --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,56 @@ +""" +Test functions in main.py +""" +import tisane as ts + +import pandas as pd +import os +import unittest + +class TestDriver(unittest.TestCase): + def testMultiverseGeneration(self): + dir = os.path.relpath("examples/Animal_Science/") + df = pd.read_csv(os.path.join(dir, "pigs.csv")) + + ## Initialize variables with data + # Bind measures to units at the time of declaration + week = ts.SetUp("Time", order=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], cardinality=12) + pig = ts.Unit("Pig", cardinality=72) # 72 pigs + litter = ts.Unit("Litter", cardinality=21) # 21 litters + # Each pig has 1 instance of an ordinal Evit measure + vitamin_e = pig.ordinal( + "Evit", order=["Evit000", "Evit100", "Evit200"], number_of_instances=1 + ) + # Each pig has 1 instance of an ordinal Cu measure + copper = pig.ordinal("Cu", order=["Cu000", "Cu035", "Cu175"], number_of_instances=1) + # Each pig has for each value of week 1 instance of a numeric Weight measure + # Also: Each pig has 1 instance of a Weight measure corresponding to each week + weight = pig.numeric("Weight", number_of_instances=week) + # Each pig has for each value of week 1 instance of a numeric Feed consumption measure + feed = pig.numeric("Feed consumption", number_of_instances=week) + + ## Conceptual relationships + week.causes(weight) + + ## Data measurement relationships + # Pigs are nested within litters + pig.nests_within(litter) + + ## Specify and execute query + design = ts.Design(dv=weight, ivs=[week]).assign_data(df) + + # ts.infer_statistical_model_from_design(design=design) + + ts.infer_all_models(design=design) + + + def test_multiverse_decisions_generation(self): + file = "decisions.json" + pass + + def test_multiverse_template_generation(self): + file = "template.py" + pass + + + From 49e0f0b5eab57176b4602cf9a8db831dc2013b20 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 09:41:32 -0700 Subject: [PATCH 06/19] Start new function for adding multiverse code generation using Boba --- tisane/main.py | 4 ++++ tisane/multiverse_code_generator.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tisane/multiverse_code_generator.py diff --git a/tisane/main.py b/tisane/main.py index a3e2c5c..91f8155 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -578,6 +578,10 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal # Generate template file for the multiverse # There is only one template file generated because Tisane enforces the inclusion of mixed effects if they are inferred. template_file = "template.py" + # TODO: Don't call generate_code (like would for a single model) because + # (i) we are using boba to generate the combinatorial set and + # (ii) therefore are not constructing complete StatisticalModels, which is the input for generate_code/functions in code_generator.py + # Could imagine feeding a partial spec into code_generator and having it fill the rest out, but that seems like a different use case to design for... generate_template_code(template_file, decisions_file, data_file) # Need to inject decisions into template file to use boba ### TODO: Step 4: Generate bash script/output for how to execute Boba? diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py new file mode 100644 index 0000000..00658e9 --- /dev/null +++ b/tisane/multiverse_code_generator.py @@ -0,0 +1,19 @@ +from tisane.family import SquarerootLink +from tisane.data import Dataset +from tisane.variable import AbstractVariable +from tisane.statistical_model import StatisticalModel +from tisane.random_effects import ( + RandomIntercept, + RandomSlope, + CorrelatedRandomSlopeAndIntercept, + UncorrelatedRandomSlopeAndIntercept, +) + +import os +from typing import List, Any, Tuple +import typing +import pandas as pd +import statsmodels.api as sm +import statsmodels.formula.api as smf + +# TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... \ No newline at end of file From 74ad3f4ebf46a7b63e79c9b753becbe7e92f5ebd Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 18:50:41 -0700 Subject: [PATCH 07/19] Change template code generation --- tisane/main.py | 12 +++++------ tisane/multiverse_code_generator.py | 32 +++++++++++++++++++---------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/tisane/main.py b/tisane/main.py index 91f8155..956e8a3 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -564,8 +564,12 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal "associative intermediary main effects" ] = associative_intermediaries - ### Step 3: Generate multiverse code + has_random_effects = False + # Are there any random effects? If so, update has_random_effects. + if len(random_effects_candidates) > 0: + has_random_effects = True + # Generate the dicitonary representing the multiverse decisions_file = "decisions.json" construct_multiverse_decisions(combined_dict, decisions_file) @@ -582,10 +586,6 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal # (i) we are using boba to generate the combinatorial set and # (ii) therefore are not constructing complete StatisticalModels, which is the input for generate_code/functions in code_generator.py # Could imagine feeding a partial spec into code_generator and having it fill the rest out, but that seems like a different use case to design for... - generate_template_code(template_file, decisions_file, data_file) # Need to inject decisions into template file to use boba + generate_template_code(template_file, decisions_file, data_file, has_random_effedcts=False) # Need to inject decisions into template file to use boba ### TODO: Step 4: Generate bash script/output for how to execute Boba? - -# TODO: Should this output a Boba script? -def infer_multiverse(): - pass \ No newline at end of file diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index 00658e9..d82f1cb 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -1,13 +1,4 @@ -from tisane.family import SquarerootLink -from tisane.data import Dataset -from tisane.variable import AbstractVariable -from tisane.statistical_model import StatisticalModel -from tisane.random_effects import ( - RandomIntercept, - RandomSlope, - CorrelatedRandomSlopeAndIntercept, - UncorrelatedRandomSlopeAndIntercept, -) +from tisane.code_generator import generate_python_code import os from typing import List, Any, Tuple @@ -16,4 +7,23 @@ import statsmodels.api as sm import statsmodels.formula.api as smf -# TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... \ No newline at end of file +# TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... + + +# @param template_file is the output file where the code will be output +def generate_template_code(template_file: os.PathLike, decisions_file: os.PathLike, data_file: os.PathLike, target: str = "PYTHON", has_random_effects: bool = False): + if target.upper() == "PYTHON": + return generate_template_python_code(template_file, decisions_file, data_file) + + #else + assert(target.upper() == "R") + return generate_template_r_code(template_file, decisions_file, data_file) + +# TODO: Need to inject decisions into template file to use boba +def generate_template_python_code(): + # IF THERE ARE RANDOM EFFECTS USE.... + pass + +def generate_template_r_code(): + # Output file is an R file + pass \ No newline at end of file From fbd022337bf7b7129922d96a4e635f2bdef136f0 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 22:21:30 -0700 Subject: [PATCH 08/19] Initial implementation of formula generation. Moving towards more modular generation and leaving formula specification to Boba. --- tisane/main.py | 3 +- tisane/multiverse_code_generator.py | 97 ++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/tisane/main.py b/tisane/main.py index 956e8a3..8558c46 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -564,6 +564,7 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal "associative intermediary main effects" ] = associative_intermediaries + import pdb; pdb.set_trace() ### Step 3: Generate multiverse code has_random_effects = False # Are there any random effects? If so, update has_random_effects. @@ -572,7 +573,7 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal # Generate the dicitonary representing the multiverse decisions_file = "decisions.json" - construct_multiverse_decisions(combined_dict, decisions_file) + generate_multiverse_decisions(combined_dict, "./", decisions_file) # Output data somewhere to read in from template.py data = design.get_data() diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index d82f1cb..1346a16 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -1,13 +1,106 @@ from tisane.code_generator import generate_python_code import os -from typing import List, Any, Tuple -import typing +from pathlib import Path +from itertools import chain, combinations +from typing import List, Any, Tuple, Dict +import json import pandas as pd import statsmodels.api as sm import statsmodels.formula.api as smf +### GLOBALs +formula_generation = """ +ivs = list() +ivs.append({{main_effects}}) +ivs.append({{interaction_effects}}) +ivs.append({{random_effects}}) +ivs_formula = "+".join(ivs) +dv_formula = "{{dependent_variable}} ~ " +formula = dv_formula + ivs_formula +""" + +### Helper functions +def powerset(iterable, min_length=0, max_length=None): + "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" + s = list(iterable) + if max_length is not None: + return chain.from_iterable(combinations(s, r) for r in range(min_length, max_length+1)) + #else: + return chain.from_iterable(combinations(s, r) for r in range(min_length, len(s)+1)) + +# Write data to JSON file specified in @param output_path +def write_to_json(data: Dict, output_path: str, output_filename: str): + assert output_filename.endswith(".json") + path = Path(output_path, output_filename) + # Output dictionary to JSON + with open(path, "w+", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4, sort_keys=True) + + return path + + # TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... +def construct_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: + formulae = list() + + return formulae + +def construct_all_main_options(combined_dict: Dict[str, Any]) -> List[str]: + input = combined_dict["input"] + main_effects = input["generated main effects"] + main_options = powerset(main_effects) + + return main_options + +def construct_all_interaction_options(combined_dict: Dict[str, Any]) -> List[str]: + pass + +def construct_all_family(combined_dict: Dict[str, Any]) -> List[str]: + family = list() + + return family + +def construct_all_link(combined_dict: Dict[str, Any]) -> List[str]: + link = list() + + return link + + +def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: + + # Generate formulae decisions + formulae_options = construct_all_formulae(combined_dict=combined_dict) + formulae_dict = dict() + formulae_dict["var"] = "formula" + formulae_dict["options"] = formulae_options + + # Generate family decisions + family_options = construct_all_family(combined_dict) + family_dict = dict() + family_dict["var"] = "family" + family_dict["options"] = family_options + + # Generate link decisions + link_options = construct_all_link(combined_dict) + link_dict = dict() + link_dict["var"] = "link" + link_dict["options"] = link_options + + # Combine all the decisions + decisions_dict = dict() + decisions_dict["graph"] = list() + decisions_dict["decisions"] = list() + decisions_dict["decisions"].append(formulae_dict) + decisions_dict["decisions"].append(family_dict) + decisions_dict["decisions"].append(link_dict) + # TODO: Add any bash commands? + # decisions_dict["before_execute"] = "cp ./code/" + + # Write out JSON + path = write_to_json(data=decisions_dict, output_path=decisions_path, output_filename=decisions_file) + + return path # @param template_file is the output file where the code will be output From 0fb94953c932669e81030ed7094acbfbdb889860 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 22:21:57 -0700 Subject: [PATCH 09/19] Start tests for multiverse code generation helpers --- .../test_generate_multiverse_code_helpers.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_generate_multiverse_code_helpers.py diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py new file mode 100644 index 0000000..3585204 --- /dev/null +++ b/tests/test_generate_multiverse_code_helpers.py @@ -0,0 +1,67 @@ +from tisane.multiverse_code_generator import construct_all_main_options + +import os +import unittest +import json + +test_input_repo_name = "input_json_files/" +dir = os.path.dirname(__file__) +dir = os.path.join(dir, test_input_repo_name) + +class MultiverseCodeHelpers(unittest.TestCase): + + def test_construct_all_formulae_main_only(self): + main_only_file = "main_only.json" + + input_filename = "main_only.json" + input_path = os.path.join(dir, input_filename) + + # Read in JSON file as a dict + with open(input_path, "r") as f: + file_data = f.read() + combined_dict = json.loads(file_data) + + + def test_construct_all_main_options(self): + main_only_file = "main_only.json" + + input_filename = "main_only.json" + input_path = os.path.join(dir, input_filename) + + # Read in JSON file as a dict + with open(input_path, "r") as f: + file_data = f.read() + combined_dict = json.loads(file_data) + + main_options = list(construct_all_main_options(combined_dict=combined_dict)) + self.assertEqual(len(main_options), 2) + for mo in main_options: + self.assertLessEqual(len(mo), 1) + + if len(mo) == 1: + self.assertIn("Time", mo) + + def test_construct_all_formulae_interaction_only(self): + combined_dict = dict() + pass + + def test_construct_all_formulae_main_interaction(self): + combined_dict = dict() + pass + + def test_construct_all_formulae_main_random(self): + combined_dict = dict() + pass + + def test_construct_all_formulae_main_interaction_random(self): + combined_dict = dict() + pass + + def test_construct_all_family(self): + pass + + def test_construct_all_link(self): + pass + + def test_generate_multiverse_decisions(self): + pass From 4fdcadfe17c1c7854cbdedc9b092e61de1ba8732 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 22:39:58 -0700 Subject: [PATCH 10/19] Construct effects separartely, add tests for that --- .../test_generate_multiverse_code_helpers.py | 42 ++++++++++++++---- tisane/multiverse_code_generator.py | 44 ++++++++++++++----- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 3585204..5ef86ba 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -1,4 +1,4 @@ -from tisane.multiverse_code_generator import construct_all_main_options +from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options import os import unittest @@ -22,9 +22,7 @@ def test_construct_all_formulae_main_only(self): combined_dict = json.loads(file_data) - def test_construct_all_main_options(self): - main_only_file = "main_only.json" - + def test_construct_all_main_options_main_only(self): input_filename = "main_only.json" input_path = os.path.join(dir, input_filename) @@ -35,12 +33,40 @@ def test_construct_all_main_options(self): main_options = list(construct_all_main_options(combined_dict=combined_dict)) self.assertEqual(len(main_options), 2) - for mo in main_options: - self.assertLessEqual(len(mo), 1) + for o in main_options: + self.assertLessEqual(len(o), 1) + + if len(o) == 1: + self.assertIn("Time", o) + + def test_construct_all_interaction_options_main_only(self): + input_filename = "main_only.json" + input_path = os.path.join(dir, input_filename) + + # Read in JSON file as a dict + with open(input_path, "r") as f: + file_data = f.read() + combined_dict = json.loads(file_data) + + interaction_options = list(construct_all_interaction_options(combined_dict=combined_dict)) + self.assertEqual(len(interaction_options), 1) + for o in interaction_options: + self.assertLessEqual(len(o), 0) - if len(mo) == 1: - self.assertIn("Time", mo) + def test_construct_all_random_options_main_only(self): + input_filename = "main_only.json" + input_path = os.path.join(dir, input_filename) + + # Read in JSON file as a dict + with open(input_path, "r") as f: + file_data = f.read() + combined_dict = json.loads(file_data) + random_options = list(construct_all_random_options(combined_dict=combined_dict)) + self.assertEqual(len(random_options), 1) + for o in random_options: + self.assertLessEqual(len(o), 0) + def test_construct_all_formulae_interaction_only(self): combined_dict = dict() pass diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index 1346a16..c1219c0 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -41,7 +41,7 @@ def write_to_json(data: Dict, output_path: str, output_filename: str): # TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... -def construct_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: +def generate_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: formulae = list() return formulae @@ -54,7 +54,18 @@ def construct_all_main_options(combined_dict: Dict[str, Any]) -> List[str]: return main_options def construct_all_interaction_options(combined_dict: Dict[str, Any]) -> List[str]: - pass + input = combined_dict["input"] + interaction_effects = input["generated interaction effects"] + interaction_options = powerset(interaction_effects) + + return interaction_options + +def construct_all_random_options(combined_dict: Dict[str, Any]) -> List[str]: + input = combined_dict["input"] + random_effects = input["generated random effects"] + random_options = powerset(random_effects) + + return random_options def construct_all_family(combined_dict: Dict[str, Any]) -> List[str]: family = list() @@ -69,19 +80,32 @@ def construct_all_link(combined_dict: Dict[str, Any]) -> List[str]: def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: - # Generate formulae decisions - formulae_options = construct_all_formulae(combined_dict=combined_dict) - formulae_dict = dict() - formulae_dict["var"] = "formula" - formulae_dict["options"] = formulae_options - - # Generate family decisions + # Generate formulae decisions modularly + # Construct main options + main_options = construct_all_main_options(combined_dict=combined_dict) + main_dict = dict() + main_dict["var"] = "main_effects" + main_dict["options"] = main_options + + # Construct interaction options + interaction_options = construct_all_interaction_options(combined_dict=combined_dict) + interaction_dict = dict() + interaction_dict["var"] = "interaction_effects" + interaction_dict["options"] = interaction_options + + # Construct random options + random_options = construct_all_random_options(combined_dict=combined_dict) + random_dict = dict() + random_dict["var"] = "random_effects" + random_dict["options"] = random_options + + # Construct family decisions family_options = construct_all_family(combined_dict) family_dict = dict() family_dict["var"] = "family" family_dict["options"] = family_options - # Generate link decisions + # Construct link decisions link_options = construct_all_link(combined_dict) link_dict = dict() link_dict["var"] = "link" From b072aa14794c21355e6e8e4a7536d64385049b3f Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Wed, 3 Nov 2021 23:24:33 -0700 Subject: [PATCH 11/19] Add family and link pair construction and testing --- .../test_generate_multiverse_code_helpers.py | 67 +++++++++++++++++-- tisane/multiverse_code_generator.py | 52 +++++++------- 2 files changed, 85 insertions(+), 34 deletions(-) diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 5ef86ba..32be64d 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -1,12 +1,16 @@ -from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options +## TODO: Check that the Family - link pairs make sense (this might be covered earlier in the pipeline) + +from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_multiverse_decisions import os import unittest import json test_input_repo_name = "input_json_files/" +test_output_decision_repo_name = "output_decision_json_files/" dir = os.path.dirname(__file__) -dir = os.path.join(dir, test_input_repo_name) +input_dir = os.path.join(dir, test_input_repo_name) +output_dir = os.path.join(dir, test_output_decision_repo_name) class MultiverseCodeHelpers(unittest.TestCase): @@ -14,7 +18,7 @@ def test_construct_all_formulae_main_only(self): main_only_file = "main_only.json" input_filename = "main_only.json" - input_path = os.path.join(dir, input_filename) + input_path = os.path.join(input_dir, input_filename) # Read in JSON file as a dict with open(input_path, "r") as f: @@ -24,7 +28,7 @@ def test_construct_all_formulae_main_only(self): def test_construct_all_main_options_main_only(self): input_filename = "main_only.json" - input_path = os.path.join(dir, input_filename) + input_path = os.path.join(input_dir, input_filename) # Read in JSON file as a dict with open(input_path, "r") as f: @@ -41,7 +45,7 @@ def test_construct_all_main_options_main_only(self): def test_construct_all_interaction_options_main_only(self): input_filename = "main_only.json" - input_path = os.path.join(dir, input_filename) + input_path = os.path.join(input_dir, input_filename) # Read in JSON file as a dict with open(input_path, "r") as f: @@ -55,7 +59,7 @@ def test_construct_all_interaction_options_main_only(self): def test_construct_all_random_options_main_only(self): input_filename = "main_only.json" - input_path = os.path.join(dir, input_filename) + input_path = os.path.join(input_dir, input_filename) # Read in JSON file as a dict with open(input_path, "r") as f: @@ -66,7 +70,58 @@ def test_construct_all_random_options_main_only(self): self.assertEqual(len(random_options), 1) for o in random_options: self.assertLessEqual(len(o), 0) + + def test_construct_family_link_pairs(self): + input_filename = "main_only.json" + input_path = os.path.join(input_dir, input_filename) + # Read in JSON file as a dict + with open(input_path, "r") as f: + file_data = f.read() + combined_dict = json.loads(file_data) + + family_link_options = list(construct_all_family_link_options(combined_dict=combined_dict)) + for o in family_link_options: + self.assertEqual(len(o), 2) + + family = o[0] + link = o[1] + self.assertIn("Family", family) + self.assertIn("Link", link) + + def test_generate_multiverse_decisions_main_only(self): + input_filename = "main_only.json" + input_path = os.path.join(input_dir, input_filename) + + # Read in JSON file as a dict + with open(input_path, "r") as f: + file_data = f.read() + input_dict = json.loads(file_data) + + output_filename = "decisions_main_only.json" + output_path = generate_multiverse_decisions(combined_dict=input_dict, decisions_path=output_dir, decisions_file=output_filename) + + # Read in JSON file as a dict + with open(output_path, "r") as f: + file_data = f.read() + output_dict = json.loads(file_data) + decisions = output_dict["decisions"] + + # Do we need a more extensive test here? + # for dec in decisions: + # var = dec["var"] + # if var == "main_effects": + # options = dec["options"] + # input_dict["input"]["generated main effects"] + # elif var == "interaction_effects": + # pass + # elif var == "random_effects": + # pass + # elif var == "family, link pairs": + # pass + + + # TODO: Add more tests def test_construct_all_formulae_interaction_only(self): combined_dict = dict() pass diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index c1219c0..1c53d53 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -20,6 +20,11 @@ formula = dv_formula + ivs_formula """ +family_link_specification = """ +family = {{family_link_pair}}[0] +link = {{family_link_pair}}[1] +""" + ### Helper functions def powerset(iterable, min_length=0, max_length=None): "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" @@ -39,7 +44,6 @@ def write_to_json(data: Dict, output_path: str, output_filename: str): return path - # TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... def generate_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: formulae = list() @@ -51,35 +55,33 @@ def construct_all_main_options(combined_dict: Dict[str, Any]) -> List[str]: main_effects = input["generated main effects"] main_options = powerset(main_effects) - return main_options + return list(main_options) def construct_all_interaction_options(combined_dict: Dict[str, Any]) -> List[str]: input = combined_dict["input"] interaction_effects = input["generated interaction effects"] interaction_options = powerset(interaction_effects) - return interaction_options + return list(interaction_options) def construct_all_random_options(combined_dict: Dict[str, Any]) -> List[str]: input = combined_dict["input"] random_effects = input["generated random effects"] random_options = powerset(random_effects) - return random_options - -def construct_all_family(combined_dict: Dict[str, Any]) -> List[str]: - family = list() - - return family + return list(random_options) -def construct_all_link(combined_dict: Dict[str, Any]) -> List[str]: - link = list() +def construct_all_family_link_options(combined_dict: Dict[str, Any]) -> List[List[str]]: + input = combined_dict["input"] - return link + family_link_options = list() + for family, links in input["generated family, link functions"].items(): + for l in links: + family_link_options.append([family, l]) + return family_link_options def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: - # Generate formulae decisions modularly # Construct main options main_options = construct_all_main_options(combined_dict=combined_dict) @@ -99,25 +101,20 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: random_dict["var"] = "random_effects" random_dict["options"] = random_options - # Construct family decisions - family_options = construct_all_family(combined_dict) - family_dict = dict() - family_dict["var"] = "family" - family_dict["options"] = family_options - - # Construct link decisions - link_options = construct_all_link(combined_dict) - link_dict = dict() - link_dict["var"] = "link" - link_dict["options"] = link_options + # Construct family and link pair decisions + family_link_options = construct_all_family_link_options(combined_dict) + family_link_dict = dict() + family_link_dict["var"] = "family, link pairs" + family_link_dict["options"] = family_link_options # Combine all the decisions decisions_dict = dict() decisions_dict["graph"] = list() decisions_dict["decisions"] = list() - decisions_dict["decisions"].append(formulae_dict) - decisions_dict["decisions"].append(family_dict) - decisions_dict["decisions"].append(link_dict) + decisions_dict["decisions"].append(main_dict) + decisions_dict["decisions"].append(interaction_dict) + decisions_dict["decisions"].append(random_dict) + decisions_dict["decisions"].append(family_link_dict) # TODO: Add any bash commands? # decisions_dict["before_execute"] = "cp ./code/" @@ -126,7 +123,6 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: return path - # @param template_file is the output file where the code will be output def generate_template_code(template_file: os.PathLike, decisions_file: os.PathLike, data_file: os.PathLike, target: str = "PYTHON", has_random_effects: bool = False): if target.upper() == "PYTHON": From 6b14fbf32097fb1199ca923a4be8e569658d57e6 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Thu, 4 Nov 2021 21:26:41 -0700 Subject: [PATCH 12/19] Add test files --- tests/input_json_files/main_interaction.json | 42 +++++++ tests/input_json_files/main_only.json | 41 +++++++ .../decisions_main_only.json | 111 ++++++++++++++++++ .../test_generate_multiverse_code_helpers.py | 1 - 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/input_json_files/main_interaction.json create mode 100644 tests/input_json_files/main_only.json create mode 100644 tests/output_decision_json_files/decisions_main_only.json diff --git a/tests/input_json_files/main_interaction.json b/tests/input_json_files/main_interaction.json new file mode 100644 index 0000000..4c556d6 --- /dev/null +++ b/tests/input_json_files/main_interaction.json @@ -0,0 +1,42 @@ +{ + "input": { + "generated family, link functions": { + "GammaFamily": [ + "LogLink", + "InverseLink", + "IdentityLink" + ], + "GaussianFamily": [ + "LogitLink", + "InverseLink", + "ProbitLink", + "PowerLink", + "IdentityLink", + "LogLink", + "NegativeBinomialLink", + "CLogLogLink" + ], + "InverseGaussianFamily": [ + "LogLink", + "InverseLink", + "InverseSquaredLink", + "IdentityLink" + ], + "PoissonFamily": [ + "SquarerootLink", + "LogLink", + "IdentityLink" + ], + "TweedieFamily": [ + "PowerLink", + "LogLink" + ] + }, + "generated interaction effects": [], + "generated main effects": [ + "Time", + "Species" + ], + "generated random effects": {} + } +} \ No newline at end of file diff --git a/tests/input_json_files/main_only.json b/tests/input_json_files/main_only.json new file mode 100644 index 0000000..ba5401a --- /dev/null +++ b/tests/input_json_files/main_only.json @@ -0,0 +1,41 @@ +{ + "input": { + "generated family, link functions": { + "GammaFamily": [ + "LogLink", + "InverseLink", + "IdentityLink" + ], + "GaussianFamily": [ + "LogitLink", + "InverseLink", + "ProbitLink", + "PowerLink", + "IdentityLink", + "LogLink", + "NegativeBinomialLink", + "CLogLogLink" + ], + "InverseGaussianFamily": [ + "LogLink", + "InverseLink", + "InverseSquaredLink", + "IdentityLink" + ], + "PoissonFamily": [ + "SquarerootLink", + "LogLink", + "IdentityLink" + ], + "TweedieFamily": [ + "PowerLink", + "LogLink" + ] + }, + "generated interaction effects": [], + "generated main effects": [ + "Time" + ], + "generated random effects": {} + } +} \ No newline at end of file diff --git a/tests/output_decision_json_files/decisions_main_only.json b/tests/output_decision_json_files/decisions_main_only.json new file mode 100644 index 0000000..a01137b --- /dev/null +++ b/tests/output_decision_json_files/decisions_main_only.json @@ -0,0 +1,111 @@ +{ + "decisions": [ + { + "options": [ + [], + [ + "Time" + ] + ], + "var": "main_effects" + }, + { + "options": [ + [] + ], + "var": "interaction_effects" + }, + { + "options": [ + [] + ], + "var": "random_effects" + }, + { + "options": [ + [ + "GammaFamily", + "LogLink" + ], + [ + "GammaFamily", + "InverseLink" + ], + [ + "GammaFamily", + "IdentityLink" + ], + [ + "GaussianFamily", + "LogitLink" + ], + [ + "GaussianFamily", + "InverseLink" + ], + [ + "GaussianFamily", + "ProbitLink" + ], + [ + "GaussianFamily", + "PowerLink" + ], + [ + "GaussianFamily", + "IdentityLink" + ], + [ + "GaussianFamily", + "LogLink" + ], + [ + "GaussianFamily", + "NegativeBinomialLink" + ], + [ + "GaussianFamily", + "CLogLogLink" + ], + [ + "InverseGaussianFamily", + "LogLink" + ], + [ + "InverseGaussianFamily", + "InverseLink" + ], + [ + "InverseGaussianFamily", + "InverseSquaredLink" + ], + [ + "InverseGaussianFamily", + "IdentityLink" + ], + [ + "PoissonFamily", + "SquarerootLink" + ], + [ + "PoissonFamily", + "LogLink" + ], + [ + "PoissonFamily", + "IdentityLink" + ], + [ + "TweedieFamily", + "PowerLink" + ], + [ + "TweedieFamily", + "LogLink" + ] + ], + "var": "family, link pairs" + } + ], + "graph": [] +} \ No newline at end of file diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 32be64d..6c4e90f 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -119,7 +119,6 @@ def test_generate_multiverse_decisions_main_only(self): # pass # elif var == "family, link pairs": # pass - # TODO: Add more tests def test_construct_all_formulae_interaction_only(self): From e2bcf23845823234902c60af0ab49e625495586c Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Thu, 4 Nov 2021 22:45:09 -0700 Subject: [PATCH 13/19] Add implementation for generating template file for multiverse analysis, minor edits --- .../test_generate_multiverse_code_helpers.py | 32 ++++- tisane/code_generator.py | 9 +- tisane/main.py | 5 +- tisane/multiverse_code_generator.py | 118 +++++++++++++++--- 4 files changed, 131 insertions(+), 33 deletions(-) diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 6c4e90f..31f0e61 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -1,6 +1,6 @@ ## TODO: Check that the Family - link pairs make sense (this might be covered earlier in the pipeline) -from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_multiverse_decisions +from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_multiverse_decisions, generate_template_code import os import unittest @@ -8,9 +8,11 @@ test_input_repo_name = "input_json_files/" test_output_decision_repo_name = "output_decision_json_files/" +test_output_template_repo_name = "output_template_files/" dir = os.path.dirname(__file__) input_dir = os.path.join(dir, test_input_repo_name) -output_dir = os.path.join(dir, test_output_decision_repo_name) +output_decision_dir = os.path.join(dir, test_output_decision_repo_name) +output_template_dir = os.path.join(dir, test_output_template_repo_name) class MultiverseCodeHelpers(unittest.TestCase): @@ -86,8 +88,7 @@ def test_construct_family_link_pairs(self): family = o[0] link = o[1] - self.assertIn("Family", family) - self.assertIn("Link", link) + self.assertIn(family, DataForTests.possible_families) def test_generate_multiverse_decisions_main_only(self): input_filename = "main_only.json" @@ -99,7 +100,7 @@ def test_generate_multiverse_decisions_main_only(self): input_dict = json.loads(file_data) output_filename = "decisions_main_only.json" - output_path = generate_multiverse_decisions(combined_dict=input_dict, decisions_path=output_dir, decisions_file=output_filename) + output_path = generate_multiverse_decisions(combined_dict=input_dict, decisions_path=output_decision_dir, decisions_file=output_filename) # Read in JSON file as a dict with open(output_path, "r") as f: @@ -120,7 +121,23 @@ def test_generate_multiverse_decisions_main_only(self): # elif var == "family, link pairs": # pass - # TODO: Add more tests + # TODO: Add more tests for interaction_only, main_interaction, .... + + def test_generate_multiverse_template_main_only(self): + decisions_filename = "decisions_main_only.json" + decisions_path = os.path.join(output_decision_dir, decisions_filename) + + output_filename = "template_main_only.py" + output_path = os.path.join(output_template_dir, decisions_filename) + output_path = generate_template_code(template_path=output_path, decisions_path=decisions_path, data_path="data.csv", target="PYTHON", has_random_effects=False) + + # Open output template file + # START HERE: Check template generation... + # First, run to see if tests fail? + # IDEA: maybe have a reference and then compare that the generated and reference are identical/equal? + + + def test_construct_all_formulae_interaction_only(self): combined_dict = dict() pass @@ -145,3 +162,6 @@ def test_construct_all_link(self): def test_generate_multiverse_decisions(self): pass + +class DataForTests: + possible_families = ["Gaussian", "InverseGaussian", "Gamma", "Tweedie", "Poisson", "Binomial", "NegativeBinomial"] \ No newline at end of file diff --git a/tisane/code_generator.py b/tisane/code_generator.py index 6628b38..5bf5024 100644 --- a/tisane/code_generator.py +++ b/tisane/code_generator.py @@ -203,9 +203,9 @@ def generate_python_code(statistical_model: StatisticalModel): if statistical_model.has_random_effects(): return generate_pymer4_code(statistical_model=statistical_model) - else: - assert not statistical_model.has_random_effects() - return generate_statsmodels_code(statistical_model=statistical_model) + # else + assert not statistical_model.has_random_effects() + return generate_statsmodels_code(statistical_model=statistical_model) def generate_pymer4_code(statistical_model: StatisticalModel, boba_template: bool = False): @@ -272,14 +272,13 @@ def generate_pymer4_model(statistical_model: StatisticalModel, boba_template: bo else: formula_code = generate_pymer4_formula(statistical_model=statistical_model) family_code = generate_pymer4_family(statistical_model=statistical_model) - # link_code = generate_pymer4_link(statistical_model=statistical_model) + # family_code = "\"" + generate_pymer4_family(statistical_model=statistical_model) + "\"" model_code = pymer4_code_templates["model_template"].format( formula=formula_code, family_name=family_code ) return model_code - def generate_pymer4_formula(statistical_model: StatisticalModel): global pymer4_code_templates diff --git a/tisane/main.py b/tisane/main.py index 8558c46..9d4b255 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -170,7 +170,6 @@ def write_to_script(code: str, output_dir: str, output_filename: str): print("Writing out path") return path - # @param file is the path to the JSON file from which to construct the statistical model def construct_statistical_model( filename: typing.Union[Path], @@ -478,8 +477,8 @@ def generateCode( sm.assign_data(design.dataset) # Generate code from SM code = generate_code(sm) + # Write generated code out - path = write_to_script(code, destinationDir, "model.py") return path @@ -587,6 +586,6 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal # (i) we are using boba to generate the combinatorial set and # (ii) therefore are not constructing complete StatisticalModels, which is the input for generate_code/functions in code_generator.py # Could imagine feeding a partial spec into code_generator and having it fill the rest out, but that seems like a different use case to design for... - generate_template_code(template_file, decisions_file, data_file, has_random_effedcts=False) # Need to inject decisions into template file to use boba + generate_template_code(template_file, decisions_file, data_file, has_random_effedcts=has_random_effects) # Need to inject decisions into template file to use boba ### TODO: Step 4: Generate bash script/output for how to execute Boba? diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index 1c53d53..5add038 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -1,16 +1,16 @@ -from tisane.code_generator import generate_python_code +from tisane.code_generator import * import os from pathlib import Path from itertools import chain, combinations -from typing import List, Any, Tuple, Dict +from typing import List, Any, Tuple, Dict, Union import json import pandas as pd import statsmodels.api as sm import statsmodels.formula.api as smf ### GLOBALs -formula_generation = """ +formula_generation_code = """ ivs = list() ivs.append({{main_effects}}) ivs.append({{interaction_effects}}) @@ -20,7 +20,7 @@ formula = dv_formula + ivs_formula """ -family_link_specification = """ +family_link_specification_code = """ family = {{family_link_pair}}[0] link = {{family_link_pair}}[1] """ @@ -44,11 +44,18 @@ def write_to_json(data: Dict, output_path: str, output_filename: str): return path -# TODO: Borrow functions from code_generator functions 9e.g., generate_pymer4_code... -def generate_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: - formulae = list() +def write_to_path(code: str, output_path: os.PathLike): + # Output @param code to .py script + with open(output_path, "w+", encoding="utf-8") as f: + f.write(code) + print("Writing out path") + return output_path + + +# def generate_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: +# formulae = list() - return formulae +# return formulae def construct_all_main_options(combined_dict: Dict[str, Any]) -> List[str]: input = combined_dict["input"] @@ -71,13 +78,25 @@ def construct_all_random_options(combined_dict: Dict[str, Any]) -> List[str]: return list(random_options) -def construct_all_family_link_options(combined_dict: Dict[str, Any]) -> List[List[str]]: +def construct_all_family_link_options(combined_dict: Dict[str, Any], has_random_effects: bool = False) -> List[List[str]]: + global pymer4_family_name_to_functions + input = combined_dict["input"] family_link_options = list() for family, links in input["generated family, link functions"].items(): for l in links: - family_link_options.append([family, l]) + if has_random_effects: + # Use pymer4 + family_func = pymer4_family_name_to_functions[family] + + link_func = l + else: + # Use statsmodels + family_func = statsmodels_family_name_to_functions[family] + link_func = statsmodels_link_name_to_functions[l] + + family_link_options.append([family_func, link_func]) return family_link_options @@ -124,19 +143,80 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: return path # @param template_file is the output file where the code will be output -def generate_template_code(template_file: os.PathLike, decisions_file: os.PathLike, data_file: os.PathLike, target: str = "PYTHON", has_random_effects: bool = False): +def generate_template_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): if target.upper() == "PYTHON": - return generate_template_python_code(template_file, decisions_file, data_file) - + code = generate_template_python_code(template_path, decisions_path, data_path, has_random_effects) #else assert(target.upper() == "R") - return generate_template_r_code(template_file, decisions_file, data_file) + code = generate_template_r_code(template_path, decisions_path, data_path, has_random_effects) + + # Write generated code out + path = write_to_path(code, template_path) + return path # TODO: Need to inject decisions into template file to use boba -def generate_template_python_code(): - # IF THERE ARE RANDOM EFFECTS USE.... - pass +def generate_template_python_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): + if has_random_effects: + return generate_template_pymer4_code(template_path, decisions_path, data_path) + #else: + return generate_template_statsmodels_code(template_path, decisions_path, data_path) -def generate_template_r_code(): +def generate_template_r_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): # Output file is an R file - pass \ No newline at end of file + raise NotImplementedError + +def generate_template_pymer4_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None]): + global pymer4_code_templates + + ### Specify preamble + preamble = pymer4_code_templates["preamble"] + + ### Generate data code + if not data_path: + data_code = pymer4_code_templates["load_data_no_data_source"] + else: + data_code = pymer4_code_templates["load_data_from_csv_template"] + data_code = data_code.format(path=str(data_path)) + + ### Generate model code + model_code = generate_template_pymer4_model(statistical_model=statistical_model) + + ### Generate model diagnostics code for plotting residuals vs. fitted + model_diagnostics_code = pymer4_code_templates["model_diagnostics"] + + ### Put everything together + model_function_wrapper = pymer4_code_templates["model_function_wrapper"] + model_diagnostics_function_wrapper = pymer4_code_templates[ + "model_diagnostics_function_wrapper" + ] + main_function = pymer4_code_templates["main_function"] + + assert data_code is not None + # Return string to write out to script + return ( + preamble + + "\n" + + model_function_wrapper + + data_code + + "\n" + + model_code + + "\n" + + model_diagnostics_function_wrapper + + model_diagnostics_code + + "\n" + + main_function + ) + +def generate_template_statsmodels_code(): + pass + +def generate_template_pymer4_model(): + global pymer4_code_templates, formula_generation_code, family_link_specification_code + + formula_code = "formula" + family_code = "{family}" + + model_code = formula_generation_code + pymer4_code_templates["model_template"].format( + formula=formula_code, family_name=family_code + ) + return model_code From 7bd84e660db4ee9aecbd94fd710de2868b8218dd Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Thu, 11 Nov 2021 16:07:45 -0800 Subject: [PATCH 14/19] Debug template code generation for statsmodels --- tests/generated_scripts/main_only.py | 1 + .../decisions_main_only.json | 80 ++++++++--------- .../test_generate_multiverse_code_helpers.py | 4 +- tisane/multiverse_code_generator.py | 85 +++++++++++++++---- 4 files changed, 112 insertions(+), 58 deletions(-) diff --git a/tests/generated_scripts/main_only.py b/tests/generated_scripts/main_only.py index 551f91b..3679997 100644 --- a/tests/generated_scripts/main_only.py +++ b/tests/generated_scripts/main_only.py @@ -30,6 +30,7 @@ def show_model_diagnostics(model): res = model.fit() plt.clf() plt.grid(True) + plt.axhline(y=0, color='r', linestyle='-') plt.plot(res.predict(linear=True), res.resid_pearson, 'o') plt.xlabel("Linear predictor") plt.ylabel("Residual") diff --git a/tests/output_decision_json_files/decisions_main_only.json b/tests/output_decision_json_files/decisions_main_only.json index a01137b..ab9fb7e 100644 --- a/tests/output_decision_json_files/decisions_main_only.json +++ b/tests/output_decision_json_files/decisions_main_only.json @@ -24,84 +24,84 @@ { "options": [ [ - "GammaFamily", - "LogLink" + "Gamma", + "log()" ], [ - "GammaFamily", - "InverseLink" + "Gamma", + "inverse_power()" ], [ - "GammaFamily", - "IdentityLink" + "Gamma", + "identity()" ], [ - "GaussianFamily", - "LogitLink" + "Gaussian", + "logit()" ], [ - "GaussianFamily", - "InverseLink" + "Gaussian", + "inverse_power()" ], [ - "GaussianFamily", - "ProbitLink" + "Gaussian", + "probit()" ], [ - "GaussianFamily", - "PowerLink" + "Gaussian", + "Power()" ], [ - "GaussianFamily", - "IdentityLink" + "Gaussian", + "identity()" ], [ - "GaussianFamily", - "LogLink" + "Gaussian", + "log()" ], [ - "GaussianFamily", - "NegativeBinomialLink" + "Gaussian", + "NegativeBinomial()" ], [ - "GaussianFamily", - "CLogLogLink" + "Gaussian", + "cloglog()" ], [ - "InverseGaussianFamily", - "LogLink" + "InverseGaussian", + "log()" ], [ - "InverseGaussianFamily", - "InverseLink" + "InverseGaussian", + "inverse_power()" ], [ - "InverseGaussianFamily", - "InverseSquaredLink" + "InverseGaussian", + "inverse_squared()" ], [ - "InverseGaussianFamily", - "IdentityLink" + "InverseGaussian", + "identity()" ], [ - "PoissonFamily", - "SquarerootLink" + "Poisson", + "Power(power=.5)" ], [ - "PoissonFamily", - "LogLink" + "Poisson", + "log()" ], [ - "PoissonFamily", - "IdentityLink" + "Poisson", + "identity()" ], [ - "TweedieFamily", - "PowerLink" + "Tweedie", + "Power()" ], [ - "TweedieFamily", - "LogLink" + "Tweedie", + "log()" ] ], "var": "family, link pairs" diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 31f0e61..8de737b 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -127,8 +127,8 @@ def test_generate_multiverse_template_main_only(self): decisions_filename = "decisions_main_only.json" decisions_path = os.path.join(output_decision_dir, decisions_filename) - output_filename = "template_main_only.py" - output_path = os.path.join(output_template_dir, decisions_filename) + template_filename = "template_main_only.py" + output_path = os.path.join(output_template_dir, template_filename) output_path = generate_template_code(template_path=output_path, decisions_path=decisions_path, data_path="data.csv", target="PYTHON", has_random_effects=False) # Open output template file diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index 5add038..cf8a879 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -11,18 +11,18 @@ ### GLOBALs formula_generation_code = """ -ivs = list() -ivs.append({{main_effects}}) -ivs.append({{interaction_effects}}) -ivs.append({{random_effects}}) -ivs_formula = "+".join(ivs) -dv_formula = "{{dependent_variable}} ~ " -formula = dv_formula + ivs_formula + ivs = list() + ivs.append({{main_effects}}) + ivs.append({{interaction_effects}}) + ivs.append({{random_effects}}) + ivs_formula = "+".join(ivs) + dv_formula = "{{dependent_variable}} ~ " + formula = dv_formula + ivs_formula """ family_link_specification_code = """ -family = {{family_link_pair}}[0] -link = {{family_link_pair}}[1] + family = {{family_link_pair}}[0] + link = {{family_link_pair}}[1] """ ### Helper functions @@ -146,9 +146,9 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: def generate_template_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): if target.upper() == "PYTHON": code = generate_template_python_code(template_path, decisions_path, data_path, has_random_effects) - #else - assert(target.upper() == "R") - code = generate_template_r_code(template_path, decisions_path, data_path, has_random_effects) + else: + assert(target.upper() == "R") + code = generate_template_r_code(template_path, decisions_path, data_path, has_random_effects) # Write generated code out path = write_to_path(code, template_path) @@ -179,7 +179,7 @@ def generate_template_pymer4_code(template_path: os.PathLike, decisions_path: os data_code = data_code.format(path=str(data_path)) ### Generate model code - model_code = generate_template_pymer4_model(statistical_model=statistical_model) + model_code = generate_template_pymer4_model() ### Generate model diagnostics code for plotting residuals vs. fitted model_diagnostics_code = pymer4_code_templates["model_diagnostics"] @@ -207,8 +207,48 @@ def generate_template_pymer4_code(template_path: os.PathLike, decisions_path: os + main_function ) -def generate_template_statsmodels_code(): - pass +def generate_template_statsmodels_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None]): + global statsmodels_code_templates + + ### Specify preamble + preamble = statsmodels_code_templates["preamble"] + + ### Generate data code + if not data_path: + data_code = statsmodels_code_templates["load_data_no_data_source"] + else: + data_code = statsmodels_code_templates["load_data_from_csv_template"] + data_code = data_code.format(path=str(data_path)) + + ### Generate model code + model_code = generate_template_statsmodels_model() + + ### Generate model diagnostics code for plotting residuals vs. fitted + model_diagnostics_code = statsmodels_code_templates["model_diagnostics"] + + ### Put everything together + model_function_wrapper = statsmodels_code_templates["model_function_wrapper"] + model_diagnostics_function_wrapper = statsmodels_code_templates[ + "model_diagnostics_function_wrapper" + ] + main_function = statsmodels_code_templates["main_function"] + + assert data_code is not None + # Return string to write out to script + return ( + preamble + + "\n" + + model_function_wrapper + + data_code + + "\n" + + model_code + + "\n" + + model_diagnostics_function_wrapper + + model_diagnostics_code + + "\n" + + main_function + ) + def generate_template_pymer4_model(): global pymer4_code_templates, formula_generation_code, family_link_specification_code @@ -218,5 +258,18 @@ def generate_template_pymer4_model(): model_code = formula_generation_code + pymer4_code_templates["model_template"].format( formula=formula_code, family_name=family_code - ) + ) + family_link_specification_code return model_code + + +def generate_template_statsmodels_model(): + global statsmodels_code_templates, formula_generation_code, family_link_specification_code + + formula_code = "formula" + family_code = "{family}" + link_code = "{link}" + + model_code = formula_generation_code + statsmodels_code_templates["model_template"].format( + formula=formula_code, family_name=family_code, link_obj=link_code + ) + family_link_specification_code + return model_code \ No newline at end of file From d90f9e07c7645d4a857fc9924f24a1e2683f5754 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Thu, 11 Nov 2021 16:09:48 -0800 Subject: [PATCH 15/19] Add generated template file --- .../template_main_only.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/output_template_files/template_main_only.py diff --git a/tests/output_template_files/template_main_only.py b/tests/output_template_files/template_main_only.py new file mode 100644 index 0000000..389d696 --- /dev/null +++ b/tests/output_template_files/template_main_only.py @@ -0,0 +1,49 @@ + +# Tisane inferred the following statistical model based on this query: {} + +import pandas as pd +import statsmodels.api as sm +import statsmodels.formula.api as smf +import matplotlib.pyplot as plt # for visualizing residual plots to diagnose model fit + + +def fit_model(): + + df = pd.read_csv('data.csv') + + + ivs = list() + ivs.append({{main_effects}}) + ivs.append({{interaction_effects}}) + ivs.append({{random_effects}}) + ivs_formula = "+".join(ivs) + dv_formula = "{{dependent_variable}} ~ " + formula = dv_formula + ivs_formula + + model = smf.glm(formula=formula, data=df, family=sm.families.{family}(sm.families.links.{link})) + res = model.fit() + print(res.summary()) + return model + + family = {{family_link_pair}}[0] + link = {{family_link_pair}}[1] + + +# What should you look for in the plot? +# If there is systematic bias in how residuals are distributed, you may want to try a new link or family function. You may also want to reconsider your conceptual and statistical models. +# Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html +def show_model_diagnostics(model): + + res = model.fit() + plt.clf() + plt.grid(True) + plt.axhline(y=0, color='r', linestyle='-') + plt.plot(res.predict(linear=True), res.resid_pearson, 'o') + plt.xlabel("Linear predictor") + plt.ylabel("Residual") + plt.show() + + +if __name__ == "__main__": + model = fit_model() + show_model_diagnostics(model) From 0f5c5cb759f75e9fa3174bebe054d4d95abbadb1 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Thu, 11 Nov 2021 16:52:37 -0800 Subject: [PATCH 16/19] Add code and test for generating boba config premable with decisions --- .../test_generate_multiverse_code_helpers.py | 30 ++++++++++++++----- tisane/multiverse_code_generator.py | 24 ++++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 8de737b..ab8d405 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -1,6 +1,6 @@ ## TODO: Check that the Family - link pairs make sense (this might be covered earlier in the pipeline) -from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_multiverse_decisions, generate_template_code +from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_boba_config_from_decisions, generate_multiverse_decisions, generate_multiverse_decisions_to_json, generate_template_code, boba_config_start, boba_config_end import os import unittest @@ -100,7 +100,7 @@ def test_generate_multiverse_decisions_main_only(self): input_dict = json.loads(file_data) output_filename = "decisions_main_only.json" - output_path = generate_multiverse_decisions(combined_dict=input_dict, decisions_path=output_decision_dir, decisions_file=output_filename) + output_path = generate_multiverse_decisions_to_json(combined_dict=input_dict, decisions_path=output_decision_dir, decisions_file=output_filename) # Read in JSON file as a dict with open(output_path, "r") as f: @@ -131,12 +131,28 @@ def test_generate_multiverse_template_main_only(self): output_path = os.path.join(output_template_dir, template_filename) output_path = generate_template_code(template_path=output_path, decisions_path=decisions_path, data_path="data.csv", target="PYTHON", has_random_effects=False) - # Open output template file - # START HERE: Check template generation... - # First, run to see if tests fail? - # IDEA: maybe have a reference and then compare that the generated and reference are identical/equal? - + def test_boba_config_from_decisions_main_only(self): + decisions_filename = "decisions_main_only.json" + decisions_path = os.path.join(output_decision_dir, decisions_filename) + # Read in JSON file as a dict + with open(decisions_path, "r") as f: + file_data = f.read() + decisions_dict = json.loads(file_data) + + boba_config_decisions = generate_boba_config_from_decisions(decisions_dict) + self.assertIsInstance(boba_config_decisions, str) + self.assertIn(boba_config_start, boba_config_decisions) + self.assertIn(boba_config_end, boba_config_decisions) + decisions = boba_config_decisions.split(boba_config_start)[1] + decisions = decisions.split(boba_config_end)[0] + decisions = decisions.rstrip() # remove trailing white space + decisions = decisions.lstrip() # remove leading white space + self.assertEqual(decisions, json.dumps(decisions_dict)) + + # TODO: After test generate_boba_config_from_decisions() + def test_generate_combined_decisions_template_main_only(self): + pass def test_construct_all_formulae_interaction_only(self): combined_dict = dict() diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index cf8a879..b5e0845 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -25,6 +25,9 @@ link = {{family_link_pair}}[1] """ +boba_config_start = "# --- (BOBA_CONFIG)" +boba_config_end = "# --- (END)" + ### Helper functions def powerset(iterable, min_length=0, max_length=None): "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" @@ -100,7 +103,7 @@ def construct_all_family_link_options(combined_dict: Dict[str, Any], has_random_ return family_link_options -def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: +def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> Dict[str, Any]: # Generate formulae decisions modularly # Construct main options main_options = construct_all_main_options(combined_dict=combined_dict) @@ -137,6 +140,11 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: # TODO: Add any bash commands? # decisions_dict["before_execute"] = "cp ./code/" + return decisions_dict + +def generate_multiverse_decisions_to_json(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: + decisions_dict = generate_multiverse_decisions(combined_dict=combined_dict, decisions_path=decisions_path, decisions_file=decisions_file) + # Write out JSON path = write_to_json(data=decisions_dict, output_path=decisions_path, output_filename=decisions_file) @@ -207,12 +215,26 @@ def generate_template_pymer4_code(template_path: os.PathLike, decisions_path: os + main_function ) +def generate_boba_config_from_decisions(decisions: Dict[str, Any]) -> str: + global boba_config_start, boba_config_end + + return ( + boba_config_start + + "\n" + + json.dumps(decisions) + + "\n" + + boba_config_end + ) + def generate_template_statsmodels_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None]): global statsmodels_code_templates ### Specify preamble preamble = statsmodels_code_templates["preamble"] + ### Generate Boba config with decisions from decisions_path file + # boba_config = generate_boba_config_from_decisions(decisions=decisions) + ### Generate data code if not data_path: data_code = statsmodels_code_templates["load_data_no_data_source"] From 0d210542847adf914d6a004cde3be4981873a87c Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Fri, 19 Nov 2021 15:49:39 -0800 Subject: [PATCH 17/19] Push local edits --- tisane/multiverse_code_generator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index b5e0845..2003712 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -233,6 +233,8 @@ def generate_template_statsmodels_code(template_path: os.PathLike, decisions_pat preamble = statsmodels_code_templates["preamble"] ### Generate Boba config with decisions from decisions_path file + # TODO: START HERE: Update so that templates contain boba config + # 2. Run boba with template ... # boba_config = generate_boba_config_from_decisions(decisions=decisions) ### Generate data code From 7dbb66857ae01e2fef00d4bd40535e823f9a241f Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Sun, 12 Dec 2021 22:17:50 -0800 Subject: [PATCH 18/19] Add Boba config to generated Boba template file --- .../template_main_only.py | 3 ++ .../test_generate_multiverse_code_helpers.py | 21 ++++----- tisane/code_generator.py | 4 -- tisane/main.py | 8 ++-- tisane/multiverse_code_generator.py | 46 ++++++++++--------- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/tests/output_template_files/template_main_only.py b/tests/output_template_files/template_main_only.py index 389d696..4969418 100644 --- a/tests/output_template_files/template_main_only.py +++ b/tests/output_template_files/template_main_only.py @@ -6,6 +6,9 @@ import statsmodels.formula.api as smf import matplotlib.pyplot as plt # for visualizing residual plots to diagnose model fit +# --- (BOBA_CONFIG) +{"decisions": [{"options": [[], ["Time"]], "var": "main_effects"}, {"options": [[]], "var": "interaction_effects"}, {"options": [[]], "var": "random_effects"}, {"options": [["Gamma", "log()"], ["Gamma", "inverse_power()"], ["Gamma", "identity()"], ["Gaussian", "logit()"], ["Gaussian", "inverse_power()"], ["Gaussian", "probit()"], ["Gaussian", "Power()"], ["Gaussian", "identity()"], ["Gaussian", "log()"], ["Gaussian", "NegativeBinomial()"], ["Gaussian", "cloglog()"], ["InverseGaussian", "log()"], ["InverseGaussian", "inverse_power()"], ["InverseGaussian", "inverse_squared()"], ["InverseGaussian", "identity()"], ["Poisson", "Power(power=.5)"], ["Poisson", "log()"], ["Poisson", "identity()"], ["Tweedie", "Power()"], ["Tweedie", "log()"]], "var": "family, link pairs"}], "graph": []} +# --- (END) def fit_model(): diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index ab8d405..901b10e 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -100,15 +100,9 @@ def test_generate_multiverse_decisions_main_only(self): input_dict = json.loads(file_data) output_filename = "decisions_main_only.json" - output_path = generate_multiverse_decisions_to_json(combined_dict=input_dict, decisions_path=output_decision_dir, decisions_file=output_filename) - - # Read in JSON file as a dict - with open(output_path, "r") as f: - file_data = f.read() - output_dict = json.loads(file_data) - decisions = output_dict["decisions"] - - # Do we need a more extensive test here? + decisions = generate_multiverse_decisions(combined_dict=input_dict) + + # TODO: Do we need a more extensive test here? # for dec in decisions: # var = dec["var"] # if var == "main_effects": @@ -126,10 +120,14 @@ def test_generate_multiverse_decisions_main_only(self): def test_generate_multiverse_template_main_only(self): decisions_filename = "decisions_main_only.json" decisions_path = os.path.join(output_decision_dir, decisions_filename) + decisions = dict() + with open(decisions_path, "r") as f: + decisions = f.read() + decisions = json.loads(decisions) template_filename = "template_main_only.py" output_path = os.path.join(output_template_dir, template_filename) - output_path = generate_template_code(template_path=output_path, decisions_path=decisions_path, data_path="data.csv", target="PYTHON", has_random_effects=False) + output_path = generate_template_code(template_path=output_path, decisions=decisions, data_path="data.csv", target="PYTHON", has_random_effects=False) def test_boba_config_from_decisions_main_only(self): decisions_filename = "decisions_main_only.json" @@ -149,8 +147,7 @@ def test_boba_config_from_decisions_main_only(self): decisions = decisions.rstrip() # remove trailing white space decisions = decisions.lstrip() # remove leading white space self.assertEqual(decisions, json.dumps(decisions_dict)) - - # TODO: After test generate_boba_config_from_decisions() + def test_generate_combined_decisions_template_main_only(self): pass diff --git a/tisane/code_generator.py b/tisane/code_generator.py index 5bf5024..31b9ce7 100644 --- a/tisane/code_generator.py +++ b/tisane/code_generator.py @@ -363,10 +363,6 @@ def generate_pymer4_family(statistical_model: StatisticalModel) -> str: return pymer4_family_name_to_functions[sm_family_name] -# def generate_pymer4_link(statistical_model=StatisticalModel) -> str: -# return str() - - def generate_statsmodels_code(statistical_model: StatisticalModel, boba_template: bool = False): global statsmodels_code_templates diff --git a/tisane/main.py b/tisane/main.py index 9d4b255..cd2c727 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -18,6 +18,7 @@ from tisane.design import Design from tisane.statistical_model import StatisticalModel from tisane.code_generator import * +from tisane.multiverse_code_generator import generate_multiverse_decisions, generate_template_code from tisane.gui.gui import TisaneGUI from enum import Enum @@ -563,7 +564,6 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal "associative intermediary main effects" ] = associative_intermediaries - import pdb; pdb.set_trace() ### Step 3: Generate multiverse code has_random_effects = False # Are there any random effects? If so, update has_random_effects. @@ -571,8 +571,8 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal has_random_effects = True # Generate the dicitonary representing the multiverse - decisions_file = "decisions.json" - generate_multiverse_decisions(combined_dict, "./", decisions_file) + # decisions_file = "decisions.json" + decisions = generate_multiverse_decisions(combined_dict) # Output data somewhere to read in from template.py data = design.get_data() @@ -586,6 +586,6 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal # (i) we are using boba to generate the combinatorial set and # (ii) therefore are not constructing complete StatisticalModels, which is the input for generate_code/functions in code_generator.py # Could imagine feeding a partial spec into code_generator and having it fill the rest out, but that seems like a different use case to design for... - generate_template_code(template_file, decisions_file, data_file, has_random_effedcts=has_random_effects) # Need to inject decisions into template file to use boba + generate_template_code(template_file, decisions, data_file, has_random_effects=has_random_effects) # Need to inject decisions into template file to use boba ### TODO: Step 4: Generate bash script/output for how to execute Boba? diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index 2003712..b9d183d 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -54,12 +54,6 @@ def write_to_path(code: str, output_path: os.PathLike): print("Writing out path") return output_path - -# def generate_all_formulae(combined_dict: Dict[str, Any]) -> List[str]: -# formulae = list() - -# return formulae - def construct_all_main_options(combined_dict: Dict[str, Any]) -> List[str]: input = combined_dict["input"] main_effects = input["generated main effects"] @@ -103,7 +97,7 @@ def construct_all_family_link_options(combined_dict: Dict[str, Any], has_random_ return family_link_options -def generate_multiverse_decisions(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> Dict[str, Any]: +def generate_multiverse_decisions(combined_dict: Dict[str, Any]) -> Dict[str, Any]: # Generate formulae decisions modularly # Construct main options main_options = construct_all_main_options(combined_dict=combined_dict) @@ -151,34 +145,37 @@ def generate_multiverse_decisions_to_json(combined_dict: Dict[str, Any], decisio return path # @param template_file is the output file where the code will be output -def generate_template_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): +def generate_template_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): if target.upper() == "PYTHON": - code = generate_template_python_code(template_path, decisions_path, data_path, has_random_effects) + code = generate_template_python_code(template_path, decisions, data_path, has_random_effects) else: assert(target.upper() == "R") - code = generate_template_r_code(template_path, decisions_path, data_path, has_random_effects) + code = generate_template_r_code(template_path, decisions, data_path, has_random_effects) # Write generated code out path = write_to_path(code, template_path) return path -# TODO: Need to inject decisions into template file to use boba -def generate_template_python_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): +# Model template after examples, such as https://github.com/uwdata/boba/blob/master/example/fertility/template.py +def generate_template_python_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): if has_random_effects: - return generate_template_pymer4_code(template_path, decisions_path, data_path) + return generate_template_pymer4_code(template_path, decisions, data_path) #else: - return generate_template_statsmodels_code(template_path, decisions_path, data_path) + return generate_template_statsmodels_code(template_path, decisions, data_path) -def generate_template_r_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): +def generate_template_r_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): # Output file is an R file raise NotImplementedError -def generate_template_pymer4_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None]): +def generate_template_pymer4_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None]): global pymer4_code_templates ### Specify preamble preamble = pymer4_code_templates["preamble"] + ### Generate Boba config from @param decisions dict + boba_config = generate_boba_config_from_decisions(decisions=decisions) + ### Generate data code if not data_path: data_code = pymer4_code_templates["load_data_no_data_source"] @@ -204,6 +201,8 @@ def generate_template_pymer4_code(template_path: os.PathLike, decisions_path: os return ( preamble + "\n" + + boba_config + + "\n" + model_function_wrapper + data_code + "\n" @@ -226,16 +225,19 @@ def generate_boba_config_from_decisions(decisions: Dict[str, Any]) -> str: + boba_config_end ) -def generate_template_statsmodels_code(template_path: os.PathLike, decisions_path: os.PathLike, data_path: Union[os.PathLike, None]): +### Generate Boba config with decisions from decisions_path file + # [x] 1. Update so that templates contain boba config + # 2. Run boba with template ... + # boba_config = generate_boba_config_from_decisions(decisions=decisions) + +def generate_template_statsmodels_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None]): global statsmodels_code_templates ### Specify preamble preamble = statsmodels_code_templates["preamble"] - ### Generate Boba config with decisions from decisions_path file - # TODO: START HERE: Update so that templates contain boba config - # 2. Run boba with template ... - # boba_config = generate_boba_config_from_decisions(decisions=decisions) + ### Generate Boba config from @param decisions dict + boba_config = generate_boba_config_from_decisions(decisions=decisions) ### Generate data code if not data_path: @@ -262,6 +264,8 @@ def generate_template_statsmodels_code(template_path: os.PathLike, decisions_pat return ( preamble + "\n" + + boba_config + + "\n" + model_function_wrapper + data_code + "\n" From 16bd1424874491315afea97743af957732dc13f5 Mon Sep 17 00:00:00 2001 From: Eunice Jun Date: Thu, 16 Dec 2021 11:30:29 -0800 Subject: [PATCH 19/19] Debug code for generating and running multiverse code using boba --- examples/Animal_Science/model_data_path.py | 2 + examples/Animal_Science/model_df.py | 4 +- examples/Animal_Science/model_no_data.py | 4 +- .../tisane_generated_files/model_data_path.py | 2 + .../tisane_generated_files/model_df.py | 2 + .../tisane_generated_files/model_no_data.py | 2 + .../tisane_generated_files/model_data_path.py | 2 + .../tisane_generated_files/model_df.py | 2 + .../tisane_generated_files/model_no_data.py | 2 + examples/readme_dot_graph.png | Bin 41918 -> 44998 bytes examples/test_more_complex.png | Bin 33724 -> 37954 bytes examples/test_units.png | Bin 9846 -> 9632 bytes poetry.lock | 297 +++++++++++++++--- pyproject.toml | 4 +- tests/generated_scripts/main_only.py | 1 + tests/input_json_files/main_interaction.json | 3 + tests/input_json_files/main_only.json | 3 + .../decisions_main_only.json | 6 +- .../template_main_only.py | 9 +- tests/test_generate_code_helpers.py | 7 +- .../test_generate_multiverse_code_helpers.py | 2 +- tests/test_main.py | 2 - tisane/code_generator.py | 35 ++- tisane/main.py | 1 - tisane/multiverse_code_generator.py | 66 ++-- 25 files changed, 365 insertions(+), 93 deletions(-) diff --git a/examples/Animal_Science/model_data_path.py b/examples/Animal_Science/model_data_path.py index 18d4c1a..35e117e 100644 --- a/examples/Animal_Science/model_data_path.py +++ b/examples/Animal_Science/model_data_path.py @@ -12,6 +12,7 @@ def fit_model(): model = Lmer(formula='Weight ~ Time + (1|Litter) + (1|Pig) + (1|Time)', family="gaussian", data=df) + print(model.fit()) return model @@ -21,6 +22,7 @@ def fit_model(): # Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html def show_model_diagnostics(model): + plt.axhline(y=0, color='r', linestyle='-') plt.scatter(model.fits, model.residuals) plt.title("Fitted values vs. Residuals") plt.xlabel("fitted values") diff --git a/examples/Animal_Science/model_df.py b/examples/Animal_Science/model_df.py index 7e296d9..a9ec7c0 100644 --- a/examples/Animal_Science/model_df.py +++ b/examples/Animal_Science/model_df.py @@ -14,7 +14,8 @@ def fit_model(): df = pd.read_csv('/Users/emjun/Git/tisane/data.csv') # Make sure that the data path is correct - model = Lmer(formula='Weight ~ Time + (1|Litter) + (1|Pig) + (1|Time)', family="gaussian", data=df) + model = Lmer(formula='Weight ~ Time + (1|Pig) + (1|Time) + (1|Litter)', family="gaussian", data=df) + print(model.fit()) return model @@ -24,6 +25,7 @@ def fit_model(): # Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html def show_model_diagnostics(model): + plt.axhline(y=0, color='r', linestyle='-') plt.scatter(model.fits, model.residuals) plt.title("Fitted values vs. Residuals") plt.xlabel("fitted values") diff --git a/examples/Animal_Science/model_no_data.py b/examples/Animal_Science/model_no_data.py index 41ad872..22c9416 100644 --- a/examples/Animal_Science/model_no_data.py +++ b/examples/Animal_Science/model_no_data.py @@ -15,7 +15,8 @@ def fit_model(): # df = - model = Lmer(formula='Weight ~ Time + (1|Pig) + (1|Litter) + (1|Time)', family="gaussian", data=df) + model = Lmer(formula='Weight ~ Time + (1|Litter) + (1|Time) + (1|Pig)', family="gaussian", data=df) + print(model.fit()) return model @@ -25,6 +26,7 @@ def fit_model(): # Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html def show_model_diagnostics(model): + plt.axhline(y=0, color='r', linestyle='-') plt.scatter(model.fits, model.residuals) plt.title("Fitted values vs. Residuals") plt.xlabel("fitted values") diff --git a/examples/Exercise/tisane_generated_files/model_data_path.py b/examples/Exercise/tisane_generated_files/model_data_path.py index 9c3d3ac..e0b2f4a 100644 --- a/examples/Exercise/tisane_generated_files/model_data_path.py +++ b/examples/Exercise/tisane_generated_files/model_data_path.py @@ -13,6 +13,7 @@ def fit_model(): model = smf.glm(formula='endurance ~ age + exercise', data=df, family=sm.families.Gaussian(sm.families.links.identity())) + res = model.fit() print(res.summary()) return model @@ -26,6 +27,7 @@ def show_model_diagnostics(model): res = model.fit() plt.clf() plt.grid(True) + plt.axhline(y=0, color='r', linestyle='-') plt.plot(res.predict(linear=True), res.resid_pearson, 'o') plt.xlabel("Linear predictor") plt.ylabel("Residual") diff --git a/examples/Exercise/tisane_generated_files/model_df.py b/examples/Exercise/tisane_generated_files/model_df.py index 9208c00..1bffbc9 100644 --- a/examples/Exercise/tisane_generated_files/model_df.py +++ b/examples/Exercise/tisane_generated_files/model_df.py @@ -16,6 +16,7 @@ def fit_model(): model = smf.glm(formula='endurance ~ age + exercise', data=df, family=sm.families.Gaussian(sm.families.links.identity())) + res = model.fit() print(res.summary()) return model @@ -29,6 +30,7 @@ def show_model_diagnostics(model): res = model.fit() plt.clf() plt.grid(True) + plt.axhline(y=0, color='r', linestyle='-') plt.plot(res.predict(linear=True), res.resid_pearson, 'o') plt.xlabel("Linear predictor") plt.ylabel("Residual") diff --git a/examples/Exercise/tisane_generated_files/model_no_data.py b/examples/Exercise/tisane_generated_files/model_no_data.py index 7062258..8713162 100644 --- a/examples/Exercise/tisane_generated_files/model_no_data.py +++ b/examples/Exercise/tisane_generated_files/model_no_data.py @@ -17,6 +17,7 @@ def fit_model(): model = smf.glm(formula='endurance ~ age + exercise', data=df, family=sm.families.Gaussian(sm.families.links.identity())) + res = model.fit() print(res.summary()) return model @@ -30,6 +31,7 @@ def show_model_diagnostics(model): res = model.fit() plt.clf() plt.grid(True) + plt.axhline(y=0, color='r', linestyle='-') plt.plot(res.predict(linear=True), res.resid_pearson, 'o') plt.xlabel("Linear predictor") plt.ylabel("Residual") diff --git a/examples/Group_Exercise/tisane_generated_files/model_data_path.py b/examples/Group_Exercise/tisane_generated_files/model_data_path.py index 69bbfab..bdcf90d 100644 --- a/examples/Group_Exercise/tisane_generated_files/model_data_path.py +++ b/examples/Group_Exercise/tisane_generated_files/model_data_path.py @@ -12,6 +12,7 @@ def fit_model(): model = Lmer(formula='pounds_lost ~ motivation + treatment + (1|group)', family="gaussian", data=df) + print(model.fit()) return model @@ -21,6 +22,7 @@ def fit_model(): # Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html def show_model_diagnostics(model): + plt.axhline(y=0, color='r', linestyle='-') plt.scatter(model.fits, model.residuals) plt.title("Fitted values vs. Residuals") plt.xlabel("fitted values") diff --git a/examples/Group_Exercise/tisane_generated_files/model_df.py b/examples/Group_Exercise/tisane_generated_files/model_df.py index e80b343..8c27981 100644 --- a/examples/Group_Exercise/tisane_generated_files/model_df.py +++ b/examples/Group_Exercise/tisane_generated_files/model_df.py @@ -15,6 +15,7 @@ def fit_model(): model = Lmer(formula='pounds_lost ~ motivation + treatment + (1|group)', family="gaussian", data=df) + print(model.fit()) return model @@ -24,6 +25,7 @@ def fit_model(): # Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html def show_model_diagnostics(model): + plt.axhline(y=0, color='r', linestyle='-') plt.scatter(model.fits, model.residuals) plt.title("Fitted values vs. Residuals") plt.xlabel("fitted values") diff --git a/examples/Group_Exercise/tisane_generated_files/model_no_data.py b/examples/Group_Exercise/tisane_generated_files/model_no_data.py index 0f7a7d8..7bab9f6 100644 --- a/examples/Group_Exercise/tisane_generated_files/model_no_data.py +++ b/examples/Group_Exercise/tisane_generated_files/model_no_data.py @@ -16,6 +16,7 @@ def fit_model(): model = Lmer(formula='pounds_lost ~ motivation + treatment + (1|group)', family="gaussian", data=df) + print(model.fit()) return model @@ -25,6 +26,7 @@ def fit_model(): # Read more here: https://sscc.wisc.edu/sscc/pubs/RegressionDiagnostics.html def show_model_diagnostics(model): + plt.axhline(y=0, color='r', linestyle='-') plt.scatter(model.fits, model.residuals) plt.title("Fitted values vs. Residuals") plt.xlabel("fitted values") diff --git a/examples/readme_dot_graph.png b/examples/readme_dot_graph.png index 9b1ad697e2413b4f1a846f726905819520c182d8..a1707dbed351ec21fe6c6238b7a497bc38e5214f 100644 GIT binary patch literal 44998 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

}h9Y2?NZ2b8y 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�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 FV1i1l;>7>} diff --git a/examples/test_more_complex.png b/examples/test_more_complex.png index 729059d40e7fb55ea947b763f8b56ab9a0ba5f82..830d2974342853277cc5f3b839cb52a45e42cd3e 100644 GIT binary patch literal 37954 zcmb6BcQ}{-`v#6b+a(bp8HpsbD9R`bAv0T2vdSz{_GsCAmz3-kG9yKWWMyw zzUTe=d_KqT_#MahpYQv1ypIm+e%|-vaXqf*cj7w(vz?>(XQ!cptTbtp`0w?{lrR$M2(GbLnI8KG8SV%qMg9GQ?9c%02eow}Y26{^p*_z{e33 z*JA%sx=+8@cd2Ij`_@0jC$cJjblFZb_Z!&#t`i}rq7r7J@KV2O;b3{Z-�kJdh`m z_{JqY)?s|fP5qYnVL6wd-ifM^qq3cWs#bFj=lq#OLZ|434wqlI95g+hqeFiF{P{v7 zjcZ#Uk*Iy1;wQ}ZIX>#=nIznpq$^xC`zQYOx!+#x(Xz1i2rnP7PWS%*|A;{{F0b+d z4JvIp5hjWh-y+GH0c&B+3R*P^uktjutl)dXOv%SnetZQmuL@(jF%H1df?!} zU06JR@`Qz=?CiTaRn!L#ln?iv&27qPHJCdX#j* z+?;#swrz7mK@x0yeEYm8S!8~^e;{Di%sx6g8vd-QwpQVKNuc`GiUCuB5IJem=+fNK zu$z*+JlR!UU22kf`y#&=F* zJ%0@jO8y;hQ$H6O5pn6G^S@+T9<9cncXzosIlb>cc;M48!t*yICPq=plROTqsxB$H zRYXiI?8%dDAKf=!;TEfFYJ5pobad$Vv53d)+{YkIGVRJWNxH?G-RHjfa{Jbc#qUNm zbV=AM-ON)S;ohU)UyA)2AD1WXCheo=e;67T=4bV@A}D{rk7KxA%n%txW6RbR`?Je7Bv%)wRIgkik@y7#x0A@3 zgs;qr-oAai;QIPV>Mr}haCM_S+qc%e6g&9y=g(&n?&6CxeL+nLvO{x2Rs2FinfFWH z-R<*~X1n;&>2LVq%^8n3;o&r-Pw|)hZkm~0HN19=)=ElBN-n8ceq_k>u)TCmnoawg zs|gD;q7TN8aB}W8stUZMrA23GXqevWcIVEChm^80Cd&>XJmTV|t!IRVwh@avMBKTb zUwMvE)mxh(nYvgBmZ7f!8P;M^C-r6Uznm%r5e3PY=l3Q@ST)V&T|+}POVT+iu0osp z_qUUzrKM>sM*6g;U!1*5As!oqt4ZzvUlDrz@(}U9q=Vgt4-4l6r z&hc?;t45`^t}cU`n%eB_fD{ca?Utoks$wjvY3EzkH<_6{Z4FQN9I&436iA9c#Kc6d zt*w23y7%f_=j~Y6ouneiKO7Z?0yyoYYuB!MOS4JK$b2a;-$ukbmOnQyFXZLRfZz2m z65Ka8c2iQy-n@B)I377V{!gE7-%s`Gyt5}VMtN^9)9G7vpV~98HRat_A=HIFUmQN6Tz@pI znv;(&;LDdUqd1TpEG#VMJ%xOGJR~bM@1H(>+Ptsye88d8o~4@`&Sd1|iK(fa@(Kz^ zarmgHs9N7Yu&A`)zSs5>XHCYR{-RZPeq43f@s{Q!g*21e$Y0ab%0Y+Et9369lzSPQ zm|Q|c<~#qp!^T$A&~O}~wDp3PmKF^SjdV-0(otJmTiqgS){l;VJWriE^{u{MNuZ*p zr6ufZ5Q_?SQqXzP(rII@I6iaF0S?Ze?|giG20z9K+dQ6}oZL-v{ZoH-0o#Ff-QU&I zbAYrmT{6RY`gC4}|M2JQ1s%^wp5ERC+~3m{jZYn#?kfwzhO>!_A3l5cr!)@t{GUHt zT#3W3(e%KW&PGoxLAt(c0Sj)52Jb&DxSp(&t%| z%PJ~f0Rj8GcVE7I*|a${un=mb#?RSe)wNeXSr>(`sIu8ZYc_0VNyWf51= z(im^c@T#j*#673zNrfQr78Vw`HRMECtUWZ0+O0?AM3O?QjpgL!CGX!CG&C}zVrC9V zQHhm2vj6piiT2UXoTJIgFUhbb-j#+GRaHiFY+SYdD+kW{ETXrsax=+GsmbY^V->>;^NP^o4M%o&WruEC(E9C zc(s(% z)3)T~Wb6cIK>*?^K{n{HX?K3w&M;fy?k7Bn1&tFO*$B=@!NL1uL~M7erO_ZPKcgZI z3=C{Jd-m+;*x2W)s$Iy_=f^v;vx^i`*b$ffXU}%@i}hRQ@70#G@F}sMVG|UjIj$V@ zDS+|pO=IJys8mLMrLHJ`?mi>DB@ZS%aT4R5|4s9<|F%x2%nEM5sq*UF=b5t2!2bT5 zy=Cq!2<_R0g$m@mBXoWP{rwM>Uy4SotQ7B5(3yZXS@(b1l9-fa zfT~&KFvr$IB@(0SA;y-;f%Alv^VV)k7x9sRpy_;TudfxlyFZnCQ3~AqC1?5b!y!~8&F=5b&D1>FS;kz)ywpo< zSf&cX<=DSq>6_v&?IrF9n;91u2gGqDhplEH+|%27{@3L~qgDq!*0xh@A-f5jl9|bO ztM1vBzB1Ya5sMf0HM|qFcKc<-9I+wQ|h|P#>3+`B`ieuujz)uekx+c zk`$r_+cLDDop(O-yCF6RWe6vJm$jYUEwT8oxK>3)ML+iUE9%=8muEs{i5`Q(CPQ-$ z#CmKj4Fz%YEmd(bYReUT!c}st2PE~1@9j7$=6W-%*N_%^I`+xubYUj@oz>~=P-gbn7L*qp?Tn0!i76>ZIXGm@wupRB!DG1A0Sx&(f7~p2DMM(r#|zmt*y=uNO-C?=~i~M$U)D&Qf4QOqCem;I-;XZLO0|Qo}JH0xp=Oi>q zOD^I7g*nV6(m6Rf+aIc_tJn6PIDI+{-v_)^96nTN*(FI{-WYmJLGDD9c3S4!xBR(p z-@e8JX%}beaz7Mv&Su@QV~5TM2LY$W$vvo&?8lA`%np=C=-w$3ZSLr(LTG>DuRGqc zpL+LpY|mFe&&S^0 zzN*6KfPn39MO-!Q$n~jmKDU%D?wjjpZ+(yPdHi_m8~w7w;^N|#vWE4zMIx@h8Rx3K zYiencLZM({W)^z(#_(sEyM9*no4sUYH*em2{YYB#-`cXRplRd7Df%|5cfP5jNSE7p z?qp+U->$5z+?i{#S0<3jhwob9YhYl@Dx(Mu?n{V*;AhWt21B^Y`8(e~;KAotONvsY zs1<2Sw;2Em?WUmcEG`zwx?bXml;Y1U7CEJ(dyymTGwKO)E8k)aHVsQut;AWMW>;jJ zXx^A(Bs)4j{-vgdY;JB&k7`}w`Sa&XYkwMSe%Bo$Famiw=PM)not~ba!q)u{n}?XD zj)ch7=)KD*93o32^O0tAK^=W|#){$G`SV1L0rGgutiZ$0!4Ype^8H&~9RU&Bv#$G5 z9hKqac*(&wiDsZKDMn;C>e(~zU6hAY-+aU_zt+sq>dZGgP?Y!R*iGBI3rh1?9`lYY z=I7DT&B)nZfeD%$D-$PA2?)et*IF{PvuT3Loc{$PA*+y(&CPf@-Z*+vFv}y|Bp?~5 zC!WvIQLK$RnezU@gqni=H-bCsJwcksn{WzIbhL&Oy*| z-k6OIZ590?dU_PO^`a7~4WqHFw%kt$2y#<3Sv2a zI7k*?Jag6p0rjo1G1zXpH_g+SDH0_j_l#xd1@iLX?Cg_0s`*&a#m0`uH_4W5R z77MIdU;o#4|JSz=`7-fi$2@+1a7_3j{ol={swQwAIr4Cd)3Tq;>=Gi@-{IV0d_d3? zr|ujmgq0{$j`U{dnris45;QTF8QUBpeuxZabk*w9T7NfJ5ucBeBQC zM@B|uu?A?Ggkt1Ns*O)EoW4bVh=D;(QIWjIMmTnMY0ijRN)-k9qROS4LYsz9hWdbu^zkXfRbwv=D84iw(RXOjpaKysG z!f9m7!V?#>INR19bD_#?!=G&WwDbFjANFI}A$Tbps;EJr4|s+j6Zck+h6 z>+0eFFv4ihyOYG#aI7DO9ak}&?93@}qh{6|xaGb=$)@(cLkxffDDDeUI$KmEa-J!! zzPa&{VPj(hCyIo+S7iq_gRfW6$;5HCf6M#(V~q$V{*UJt3{r0D6s0O{!zm zOy>?$0@X;Hnx@utYb1U6aIPR<%whH>_z-G+x8)?=?%V2_+=xgZHPpHJ^;-_YGG1*~|@IViL1^)2=rqBqUUcj#tC&#@)rqt|fp7&F*Aw zG*S1*TlZ|)vc=r253SVq@88YMUMLleGmW9n&yKh8EUm0WF1g&eaTwikBbpZCavZN{ zvh%-vlUtbRpnUY`(KCL7t=M?;{QvDD4GawE1x@$&*racpHf$-Ye{q&_7X{NNgyu~X zlS(_L%#g6KZNSC%$C`KGN=tla!LO`8WMT_fN>H@-)(P`noK_(=#vcwEal4d(3%fI^fF6yJhQNX;lNe z_fu^kgPWHerqPz$!hZbt&}dV_?4LhF-~Bce?Cj1YD?WcLVD$AS4zp#|KX#iZjbVFEKS|_C|SN1aqnm$DCPUX`4n{QZ2<}DqsD=_-1JPV166+O4} z;^b$nYP!o^v7?;N8(9y3UlYFbG zkwg#(-26mJ8UX`Dz;DlejE}2^43Nu*-EOc`vvPBjAp^fHER?}n035zDx&HL&(|%;Z z(dMMRi0F)*7uyb>ciIl%X4(063%Xm*MHkIQCCz_(U;lIs3kyqV^5Z@AoE&Z0%E}7E zL|*QO*g=nU><9%N9i6#_zEXQ@s@VeIv?TESeN>KnWxs(Px4-T*uCV=G#|rH4^yl}! zmm;>F^q#8G>&`%xG)zp2fGhWB`!AyDRC98@+HjDLF0`aXj9K))H_nX;8buBxBO_6# zg;c{ANY-I5UNBB|7x)2$$PmmCj=Va8<&wQZl7pk8>Yv~B3VP=vz3u^z-Lez{WSDxl)pr&c5*xD{9t&3J?aUf;Q~3*(&lG6=`LziiHb>hs1ef zRrS)QoQ4*TLmF;9NnAg|RXt)|Qrf)lETS^j}q({s@I15mQJukYiaAWG0{2Dfjg2nrh2M)G~Dtu6obXj@Z(h4u&c z%^PEXlg_g+GVb+}-U{TM?}{>l*8igQ=7uZjVMs_w^O}tOHMX>R!rW$rE-K$5uL(uzaV zFSP8H_ddF?VN?&pF5Id+;uhb}KJ zF1EhDxJ_PJIk}=jIvud8S#uCrhU@#x@ULIt%WH_TJ*1Zs?lMwOJ$NlLRe0`GG%GySm4~G$zGKdC@udR#kZc?x5jmLWMPJOV_Lznlr`2 zg@=VnNyY`Ee3&9$h|C#%&N0_8wK3&YRJF07`-aQPWM0F!j*ch}*xthbIf?)obeOHJyH>yxL5C z{rchH&`>SY=CfC?l=tk}Q~A|ao}F4=Nr|t+nKuZnD-!8}YUA8{R{GCf6Kd8a6N_ET zm-9iFs=2tF>k;UrzLGF)neGoHX*gKngTs?Sd%860*WZZ_)5h0Zh!O{eFOON)2wZ~K z-O}Zq79Cl`T9T>ppym3}i4l+H9}oa&Yt}odf&=tKn(Z^nXIJ2t=-0200NMB-J|8{B z$?$jTUEd)3&q^8Qx0l$d4RAF6V-{4oE%8_18XBsA^*aN<`0DD*y%4h4>Fm5p?bXss z2UhyYyLV@iR67IX1<)xL+0Pt?kfJp>d-e>-odMjRu;V;?k1BT^8X+W=N|Zl*JaM5d zuK~0{tKJsd$#X)#)E|Y`V7xUAXP0x^wrzSPcKh^Q=63X`hLayYM~~*ovMQAZ1g@5H z5SpWaf6My1&Vhmw5-jMT7{(?Lr2lnStBY3P(ajT8tO&NQ_x*#tpuTU3k$I^bC`9sJ zR8yly((Q~<&=G40LEWIk+0^Q#OT9BaJ^l9GJJ~B&f@M}(=&+Z&_w4zMgO8gX(5M0X zKonB+Q*WcCCdkb0-8+Tc>WOtvKo7t$7G^-N_~OhyWXX$mc7-x4tvk^Pfp(-9v*Ybi z6^@Y~YtLkC&(@~|OwqD6{UPgItQYuF%-#?W43O5OQ-)7`TToC5te|b7{EYI@sqLin z;mUm!vVNw0vd)Ktya=)?-+SvrAD=x$4~-x=_@X+EqXzZf*q9yI09#;JTwF|$pFINR zNr;63RCLR#D`J^EiI($!|9T+tYh~$G+tC#4-M4QC6<46VMS4)m2{d;#cHz2a8*9r; zD-+onQ{%6PD`}hE1PyS>_Q8Z!64#u0JXky+G!=`lSJD+OTp*p$E4pH_%Qw%e z+;g9IU|?3{*+Uvn+g~0vd7;Q56?NkJ>x6_SXcM1QWjMN9fJ|2fyEystBO}q#SE*+7 z_UaM6EN;_MHZ2&S)UrE&59AB1f{B=N1b&w5CLmYed0v(~M6PqPRN6(Hg}_AEF!RY% z6^JDdMGm5xbm6(be*M~(g~cf?6(?F^@EC;&8iyD!X>f$GN=WD%t>e7UWr6j`w(*8_U64trd?$NJT;~#D~$Cv(u27Ezb{I%BuPZS+oZy zC7Bc-x6YyfKkd4?{%>Jp-5E=H8I129(%9%I=dI1nnJ6&a%PT9n%n=Fx*8-Q-!5ar4 z9*u@;B82TGb$oijS-pPy_DXp*_i^-S>nndV1QMw3+1d)2HXeZ-meWmnwI}a3Y?uWXxpyHmAO|2 z^kjSX7>F50JoWTM#SVY=?3*2p=74y2)WM*ta)K89=xk3;&rd>iEsxT7^C2SN8;s6d z6sSU@NR(_ce2^4wrYle_IX^$2;jCIb8~Qv0OG~}Mv4d>si!4)Kf`S#^R7Zn>_cf#+QQBW3Ha+}}B_NW3fB<@*T|KHb`8PjrC0Idp z?7}Wf{5>{0r@;v;MhouQdU1}*w{6`!oUfkKQ8;@x@jFaV8}x%bG0{`55_j6 zMDkrT{~=rt);;*qqb--&sS6XrM^UK=Njm@TgZuYW0gR0;EfoP5+gAJ>$M-WcXD)cX zFrd#|?Mgop8X8IvL({#*3FFCa?m5*^fAA?4DASE)?(W`|gQrHc!mEvmS_sid=-G4& z5C_QO7t|8mxk_vyH?gRkyu2FS@6bO1M_htLXfyC-NBxha_)93Xo|pXSxTe!j3ktpw zFnTDI3Hi=pc|J*(Lv^6^=rHhM5W!voDUoKYO>Voox)zuRyXomMb-%mYVCPTGr}LKd z?#>Txn+H*A>O--MPe5jtHY{viG?lJ@}#6#1YtEUM~2!$Oj z>tmD*=*noxfF+=f?uxsz8#=SR~vC4f)r|$U(*} zs-YmN3_vAtk6r7brJ*@;;sk~Lbnj;n3?bp+Pl2U>{r;_G+C_n9fg~U-DjJWS1W7JK zJU$vpq2%mmP+iJ?p&esXr|J@lTR{STdfYt?h6 z(hd>xZ>Fyb(1GZZpaQW93DKsdrTHekKIBQ<@S8VpZW@$vEdVS9ms4oT-zSJxp#GNCwwg1Jtl3^bHb|8@mcJ;})t zz_oDDS?T;#H8h@rWoJEqp82GHX+_@c25W43??>l@grcRxOE^J_-q`c>{4vY`nbWK&lPtLh?DEosnE!i`pSn7A5P4MngK#x#&;ds0hXW%HZHDn z=VGvkU4fZe0M5ikp?0$&p%Qu;`n^IUE)bfcsA`T54*SdbThldph_9-L?-eGbc_6aZ z7kloeo60}fHotf8Ud^^}Es(-A2M$OP92eN{|4bEv24A*|I?Ns++!RyZx@m9U219O4 zMUpqZb4Qut2Z-~q-yAWazdc;Op5iD9?iosTqg03wTv55+=RnjANx3}mbdiRQvKW0$B z!XqLo?bsuHe0|vnN*{Nl`C^dF>?+bVDm0h)IMQPeByL8&c%cCJ&Vk)Kb^3GyWasAp zI9E-c;orZTi4_BaKmO>#1$;Ld#8)o!L>P!s>A=81*TV2w9McV6f8!* z^IfdmJBD4}L%MY7QrgjAnGcW~3APBCC?D{bL8v#m5Gg7iNhTCNKjfgd(I)7Wjm^w1 zqth`F+I5XMBQD})XM%2}Ik39Ix-s?h<27ajK{g?w2ox$wz~5AXa$)lfcw7Jn4njYK zB3q?XZ4EORm{;W(;m`7$-4`I3HO5J$Au+Tq=F4EY!E=@7p3J^}@Wo*XH^l%3p?Dzf zD>5=Wp-6B8&zpjyO-x9T0?*7aRf4mOT8HXznPEzs5fouGFE6ht1WNR@;ZQu9;fmy0 zoqrfDU?C&&!DX46Kud7iD94It&hNR~($WIp=b>`vwt>MTbQ2Ue&fftqcS|Pc<`?`>&rKLu5Rx+W~07Si6n6Ri+J1YH@OFj8v z!^jZCY4hho=gyp=1A#7uZ9`HFj)@^hj)n4c0J#%r^cV$`@WUN@=`@N@hR7v8t`OQ) z&c6f}iQi?(+QHd*2$k1-Mx7(f0OSDS;y_$$Hs1nWOh_bq((doZ4lCRgbTSIT6gUw3vP?kfQ|_MoAfecHv0N~hCvZRpZ}sj(JH*tNL12%cFyq>cwj<}jChw> zZ|=hSIg(oMvf`BVxTfnnX+RID0FTyg71oO;4~2}<_jjT^6;?Z7TUqAEwMvRfJY&bk;w}# z@LHmI+WB13b9UBxZNB!TuZfiS-;LH`IAUZW&%eF%L$qMgc6r_e%$aPGm8ggaK41%~ zp{nj`#{U?W-HW@0!4l)x(A0iUjj#8`=^+3k(PV`#g4TVDCy2;T(i#@I^U-dKVI9{V z7eA-vhjZAdt?!yN=cce?x~Yo5+;`~IQ=B9vaH~Y-!BYD6s2$|fm4+=N9E>c2n;pT$ z#`fN2`IMux^8peWC1nbhKdRf^%xb?Y{YMylWPn-X!6vDq3rmA!C~x~1#rOYRcH*R> z0?DZg7wvA+j-5NzUYxPoO{%P@aG76E>w)M4;a&aKtz_M9_qo9xTV4aGrsw~FFqfO3 z&rX6t__VLLH|K*M@OQMUrurHh(yl8DY4=~GpF%Iiz<=|heD`=)-f^XuqMFC7(o~l{ zB_7QDe2IpNcJe_TCkqzOjNV1a!?vRYTA`tg9Ha0dsnA)|d zL+?aX)bfTPD=b;`o^g=r3-aBEYiPH=j`R^i%PR$NyZs@l@!^(joFXE{opul*)$4L+ zhKDZ!c@xZjdwYAt9d%8uH#!&Lx8XLk060g(loDFlI@;o~fV>Y>ag}jakCw&n5aT=d zNG&;u!}Tuze%R-Oi_=B?p&Y(IFRQYbj`s)~8$h)=+`NQKLV97qc~;dC$ipfzF>ybs za)@Ppb(Bv+A{N~zp$5qJ08Ivx8iOP@5U<5=n8jOr# zCsAm++b?9gyDF&bvIYPUkxZ0znGPS$T6#3ooaEOWV&b^h%*+aIN-7d+rCM5cfWJRG z2}aN}NmU?hbYpFpon;s^7Vn4D|~U_{uSvAjX(_Ow%Do4a9+C0!ZRoC+E`DRCl%iIFj5 z;X_i=F%r?}X*#(_&jQV%V@yr5f-QJI6>zm?_cUB4;TzXel$lY74B*TlWM@Uiy5l{B zQcvPMaX1|(8^)8&?!}MqAB;A_5wL~U?Rr3|d0(pPQF=zks3nEWI}7d#`&!(!O78sp zAObQI7$fUG?O)Um_^o7Miv!oW1JG;@tTe&!0haszc%a(sG|LjvCIkEZr=dB*b(nOf zxw+YT=<7bx1)NC2T0I#xX%zRs)|RJWM&+Y~5Dzx(dpyKson-799oC&)s$C%pzI3@a6v`0C_o(v>x$m>0P$ z=Ngu{^JMG;q*i>!zXk0UVZZnuD-o1=?VTY;7<>o#0R=08_hv_%jwbcoP>B)Va`NQK zg~_f1BpRugKTBP&P5(>T18eNV=g$wpCY2hw$jr#5uNisl{5w>2vKrq6Nwft5gb z3AW!xcZz+yXmi*cTAaML!=FEYXh+gW!33|sZLh3M0R#?GJr0?s395nV{I6KxUA&eK)2iCpqB>OxyQdQ3ei|lm_ute-~F*Ge84D^H$Ev{b_(N zR{%oxkWjRn;C!k@tpR~{1#<;s6WL{2783Yo3JQ06z7kfQLBMc(fmw?ue42__@g&qL zB9Ft0RxG-6e>en)=`$$){b&p|Epb?VBN~e%K8YCF=l{P<}(vuG>|uH zb`5LW8-4YUgGDhtzaH#Pcd2WMZs2Rp#05zRDCsTHT(KZ0xPc=Q02OkWJrZ`J9Y_I# z7FJSH@>SdsKA#AXUx^{5=}!oA1xWs>A0^sJQxGlT#iOC7j&HWpOkEgIK_m2)n4n10 z(49MwtxMot!m|Uou9f8|CDK*}fYMV#FI(i?yT3Oy zP@>fU#jj;n0&xN|J;U?EIl1!dJ}`9CF`Tj7{pj_ry7e`i@A9Ao9Gsj6!G>7M4yJx0 zcvftP96Pn%2L~!JMhp`r1X%*x6dkY5m5CAWfWW}7SQe`N`(M*5ypbpTLuV|X)%0ja zsO?PylO=;zyPDU$YUGa%tklHeyK&3XuM%P>SO*cb&8G+MUR%{XS)tIXQI=))Qr=tmajPuNyLbj7>%-T~5 zwiE4q!~_V4xRA0k_ax6HYe61%_NPRTh}yIOjuy5#eBblY+D)6$R=0? zjO%p5)d;s?+Nh;{K?zac+0Y(1!Cu3Lro006fMZalaMa;ol7uR2A{I)G zLkaPmODX;^ZhQ$={1|*1b8Xk!rZTci*(fQ4d_SOzmS(wCM9fL0K{-Qb|E;Mh z1RiY4OvX?8jD%@$CprD=B0t%e6L&Nta47@=cq)Uc`F-u(Q=$(0uo|`#&B(EZV9L}2cvL=?@I$5T_ zVb#XMAX*5z4@Gn*L<4|=eVD~iMaoAB<6Q56sU5|fUqHY>OcbUX;#Xn5*a2zyGm@5h z-M51z2iO$91~U5rJ@%u0`;T@G$^RU!>nQC6Bt+aVhOKf73zG>;kkTu~g@Nx8U3s^| zpv)311SGr4uV33!U(n>&V0$bAX#F)4qI&>&U{E#Dba$?F3`bg%DkI{ddc0@!(N6T;9Nq)sY^yX0f zBr2F0;lP=B7bAZn(S7~zL6}u8mS1fh>D!Ty;u9e*a+rlhg&2_o;jr}r2or*u5XIzB z!TK=+!oR&^`}Ql~7=W3Hm0Mm~YDC|qIk=r0tbiceNN_#F-0}a*Y0&+=YQq-d?Y6Oe z8=X`l5VU57?F_^N;;JyBA18$MlkrNg&RsRxJDzs2+9Mx*TeseNt&--Bg)(e4V0t53 zDncHz*-ns1-7tD1@1}IG<(hs$Q!oaRUl5^`o0G#z(tub!KQ|YL4UC-FEz2Fz+?u8i zBaUls@Vvvh%vIs_U)2Zj3#{U?DuB7f;1q7#vWmNDs4B?p^Wz-^6b7(MzX^2}GR*FO-T7+c%*C`JkyLf`{wGi7YjDQ#`IK0Cns+hPJ_@ z2UnoJe1n|^AUPFBl(5LWcm8*xCr6H#4t8XEa3$Wb+7LVuEDG68T>CR}LidiL+GPxj zF2aFBNPNw0ZN~`slo_f+m!=_)mMh<+mJW8_m_b1j$-Fg{is;5L+b{|sdmnUu=6UXA z8vG`g(!E`#(J*3etN3yWx?Mj5jk6A3mf$dB$}O+nEB; z&;l5w(Vmk2(PcRl)gx(X*|aHs2hrc zCSE=uEG+y9=6*Pwo`QQOR23_$eb|vp*I=FWO%iVfkGmg-#Q~ij@@ep^SAm4s^gn_O zM#;3^R1>Z$aO&|$6Q)1j3+Wb^dq4zCe?XTEwo~z?D6Mp&&OyJbq0gY!AtIcHH^Zl> zNZ7%_;U*v!Aq{lrYIx|Kki|S0=nYRq)({w5{?n(Wa9{_1|Mr9vnGjL1Q`u?~ctS2N zF3gX8L62pF9-J`0NIwN2Fa(W9FYY3Y)Rh=-ujP8)eS-;>gbHNVn-&(=UPZl0Tl5gb z&=VTguVAS7E!uhESLe~9Y`cZ1mQz;NqJd;BSh-o|Fehm?APhwdsFlQ`RW*s;Qc+Ry zrnxy8q(3-{h?xh%+|2;_4w6WJ`S?qg6e$|NO1Ox;FlqrZ7XUB>1J}q>;Hi&5iG$S& z(}$7qe4Yky^m-se@%VlO@kc05Sh3u~#x9=&$N7N?fzVOlpwkfP|G;tO?NkGpj{Snj zCpdCKf5*s@G&*{nYTd)qPZJZ_aV-CnOaKXh#!kBQU0b+Z}>#1I_Rg5<^I6=vJsfk1jmKg2e*`_+~?;&0YImP>ari7$*ZGAO3G7 zX@#yCkdOEJdm1=ZW&YQ(00k*#N{9Ooyw4w;{*n=@8l*^5Q%-aeTfi@Yrlmk6`$}?B z`(TPWRcA8LJDdz%UYw!%Y4KP}ENi9vE;Z_*|MLRW*VohT=xS}HL4AYhBM!!qaIrjl z_6&1tx@mG2Vc6T7AlLT$NF!Ft{SKadot>SXU!SI)Lbwt1%G{UpMHogM zkHA7gh%?}KZj`$CV3dfsRooRWLF}6j@Bc}>hK5_=z{PkCiEuOEtn`nLdSh9>AcnzU zS(fo-E-s9zL+l)cP-TiWaJ->*e%G(6iV6bBSGTr?gYx?f`}}I(=0=L}@gg)K0e@Fv zgCSV_`ey=CMy^_4;qUl{U&V#Be%nD2Ba|0R82uU=N~l-RwoZ>47dlb~e;wKdTKkV{ z&L-cZxsM#VF!r5U^W@Mh7mK#RkG$JKGOL-X2mvQ(d&l2}$tS_%DNg7kaaZI}0Zm-{ z#Z$U7#61=eJ_IEQ3`#>!|LuRQ{m6Nb4YgCcp5>RrN(f`6A77W=OR!{3*n4#5{y<@7 z#3;d3nJ%}GP(deOuWqsvO*8USFm#l^Gkvju+OXc)nd}pHhkXC_4}uqA$0CGe`Bm}f zY7>Us%|NMu4oO2}Ps7YyZ^!=K9YOpMC!osCKQ~-YZrj$a$aULaI;>f^bH3T*r>SZ#*O{@y)9A$`~pfoxV_ z^1oUirwmHZ*vLo<23eH73fJD-&lNcmP`e{AIskJEadB*?B3QEK`}7`2286T%{;>rK z>}qY?WrBg7p1v{8D~s7P_)9tiFN>iuotro?JUq;G!hCVG>B);zfq%HV?E~+l$${|` z}KsWWG?=63uB-Tz;>ka<99lE@La z*(GZWw+Z*Dgcpl6%|Qp$_{pU#+TR$61%5TYb&LDADBpzN|IGj?NV9yuZa>=i2s|X` z$&*1-!sf@o`Ge#lAT}h;ZS5qw;jM25i-%*DAD^+hU$KV-5akQ}+Oi^jDIua5LrsXhW;pI9>tA@`ZXun&xWS}A5=J9NY zb&FgG8INf4v1){dNlJyLV=2qi1~wVJQG2RbtoSjkEip`vp)g2d7QZ@mm5X!K<%aKx z5Ft*O^pQO*r5ojOSJ?jWcIk%i-~W3v#INeGtBP7@@WCil$ua>9s{d&-g0Q}MKb2%H zE-A^o*Kt)TA}UH2Cik=x?lB4{6G7UVU*oa``IVTFQCma4*lmgYh?hX<4l?U?FXe3X zflE<HMFl1puY6ZkeZ-ZyL0zV8c zV{%6d09)|huW!1x_BZ%ao>$JmzCm~nPz4DKF6NoWr*m~}?Cp0^VQ$2CijzCfxSIOS zwRaRS#E$+bIoo3+I1OA)FLI9^6PeIAb}A?+;D~VF1W19y@#KsWfAm%JUoY;K5<_pW zaJ}MqpP>aHdWmG-JGukoV{m;M&u?~?%)I5yT6gcC`C*c#MoT08WJhp%=T^fKRc|&1 z#z@Xc9sWfJs~oDkb+kqS4B6f}2gJ0bFBo0geo2)k#PCYu(T^u*m)8fP){8%H%`GYa z;&`4ii1Kvah|4V|ac7KHi0JC>bNzMrx#QyGA;N)%!)YPpB++6mSfnit_aE`@6-Za* zz}M|%0B#%^<}~eS5Q*_>yyfB(Ea(%~#%%DOAVoedEIb2SWN8b@#DL+v-BRJB6hF}#Ljl;yMIRKij7gtP0FxNFVt?;Bp} z>D*v%Xc2CN+qz@mTPun_D)VlTr`xx07pxw82&)g_zWjihXQ(5XSQ`{&mvK7tZL%2v zf}D6;1tuMQ2M5o79$;roRe2{@ zTT|2j>(^7@z1H^{?(XiGV7du}@z*yiM-=0KfcG%vRzKjp*9Mw{cql{}6xBgdCS`aZ z`yntkwzgITvq}X=MO8hJ+xHdj76F?<=}QXnciVnFMdXykp|aXmH((T%ECyS`rKOVwLi|39%I~)ky!bEAe?GV%&mUQBfK|``kF%5fc*= zyeelu9uj&1b+-Qd`&@9fsDX!PKJOF`;X*6p0rsU8VL?n5W5%TsD=Me1p7%9o0`-RI zB8k=?AxN~}1sDv<`e#IjabR$2kSI>vSWFv|WbZL**&nEY^MF+eXl-j7!)nB8ddA1< zV+mR@;RK|)K6<-znN0%QgsHaA-FHs>pqih7h(;QPBayncprF7E>eyf(AKW27f4=he zuBoj}L$zGMjP~5hxI$l0=25m4c#JIknIxPROfkWqfZLiog~2=@FELaKc7z{%>3_R^ zh+ZwR<1@3cXhH8$Q7dkz=WXn6$?(O7)k&CWs%Ynh9fUa`LSJgZVpsrF_aqz@Hi$7+eJUq02rC=McKiMyTMB zGLIe|3Y)-*mE)vo2EaQu{@&>?_m=%ZjtmD6%91K#vm;-ioZm!8T0lk_NL`t`Bu z>S}&Kye7&zxLD8L>3s-%3s_=cXvijTatS9yRYzwxcp$q?80Ykqqobp#rKUgLz5aLB7R_upXG&3#PJAU?{)!}1 znCXQEo&NRcYJpxrbcTWc?_iiPx5dWVS{8|)U{TZ3lIe5#qx3R)^p4_)HfCxI?)+H! z7{+|2@Xs(d7Z?Tl%5To(Y9 z!{#~L-n|D;u;X1L`2A3wjsH0$<$rCKpXKJBBD5JZQ&WQD``cv2IJAu7$%U$VAJ3^f zlk(=DCf#cByI9voT|jov*!=mWF>%(7OMEj|sdC+aJOY1MXiHdK6K` zG2g^>=1e4*bC|NP3|3=`?vPbq=}X8n5=DXDrs(~)UlMbVTYIO1^(Kg_96~aEdE;hsMq6iydKAaT{6XFl_Zot#kv>x>}XSvlTWH@Iz ze185OK?A2{WGJwQyPLlm=>X}4-d2O@n$dZEhHC%32P%nBI*6foyk99h=MES_yzxNC z2>4VCM6Y>Q?lCx)UcgyF4CG_#ofv(EKq~{lnGYZ`$25&*h~>Jhw8qt|mkAmbwnSL_ z4}sx)j_R+TASQzi51B=XG|j$!0q^~lIexe$LUDjx4mmka&`(nsb^IwLVC00V2b=*l zk9X`PUd==32~gP3Lj$^0_hM9tAHmBDEFlxCnmiC}&ZeoV3Lo)4^!`Wd4Qb_J7#_iZhg13v!Sn#1v|2P-#!lrj~xSo zW}w?DU_>TfWQ8bc8>`M>BNQh%p!#QK^t02h(x-JF`J1xXhM0go$rG;ti^nhKrp8}9 z!sfC(9EBHfRY3ACqVU>+m)3a0mx4FzP!I?nx@P?YIkn-z!N&+3jvw`JABAkFubSf+ z$bu7PSQpI0)xKi!bKp(Sk}}u`g7Sh_M)8}}&=4;K!@L`^j-J;sA&HHpqmoimXy?8n zwA;bQfh?cWy?Zi`j%(Jgy$=27QScO)z06JbJgupziT7zffw2)t>gxmHI_IV3<$;o! z(kD2f;5i=wGutqSYQXTz4BChoO6rIcl*ij4pu~L!1UAnrp43ew@f&?5r>CdC3CS$? zjpu2ogotV`jFA9rFFyp?MR?AE832uO z^G5Go#{W~-nTKAGmKGT;+La1r+DNM{LP~p5B19@m zNt+~T(?YvY)4r&TQg~ih_x*eRc#hvQ#~k-P$4tIH-_PgszLxVk&-3aq$?S#+ewS6> zf7%Q-jkoKjyLj=J8iy7a1`dO|Jdk>yF%y0D=NgqpNp{Rt5DG=sgH8HswM4P)NJ}5_ zf*^Q+GvuS{R7Z2~!b`MZ@U{=H#@@q(P`7_ie|bYLT!?q_d%!$fA9RuFhI|=$2RScN^pCpxS_)EU3c%uj5qAvJ3+Y2=Njr3@of^GQF7eH zH)Vj4(LP#NUoD3{)a(#X)aOiXD%p>R9y&86A;F5;LdaAO(<@fLQ-A%#J$J=cW>;FK z{@W>&U~k3998@vyn(0GF;dxW;+W@)nHSFx+s6^o0#2<&;`ElLi&Er!K9hz(O@e4KM zFH?jE$2P>&(ynyh{3^!`Xo|+q0B3}l`_JvphHeR&i$InE4_;k16^o9%SyzuDZF#Uu z53~b!5m+$aX-i8r!28=L4=HwqugLVzH2aD0$T?64I_v$&W`Gkz-T(M4R;jhq)B{fX zJ>+MZjzz8X_wCkTl_yA@{vP{MOmp5T)^7O;?kEU%0_&Tv_(xc}|xDTjvftM={0myc?%7*B5oF@V6lhMCbJndm+MF8%`Tg@FAH-7wU0#0X} z?-S^J{NCMI7=;hBNQvRzI8U6I#KT>z)iRH=T_p-Va2Bt( z&F`ZPG@K!*QaJi(R)RyfP+;10<=|Nmh1MsmDAPJ!+BPa+JxUYs7+{%{LA#1fqfJ-- z8@_Y!A_srdhM3UI+Dl$@YqTlrMiKVn{C5@&oI=6-bJ`4ZNbYzGW~)RC`H+eqb zr}~q<)ED$dJ-`00Qmy^SN)xvQh9o?zd0VYg8!S=q_btkJ^PFHxRbxSjt}xLE(qq6F z=YOd2YC5U{&)vdeWaM(H+c99Gkw339K5_ccI!9CWb$C>R4v*~{Y4#`t37*Y}Zt4M_ zjR6MenDG;ul=~Lg)P|`Jovd*O|t38sH=cL zkH^gKvu3?mQE%?Co2CTi&{Tdu?oNNspUf;7Y%G`*vN3xs^Bu9bNuq_MznD z!r47KnLdb!YDkChDE@=Q3rnt`$5=Z>c3^`kujRjm?`bo=(TiBw+UhZ>B%Fde+49vW z?A~Z3aiPksS_usvo`(PxuAD}3NT3QRN$#EKBVuAk@bj|L5Gyh3qiSi2{p!thF*U+3KR+LG<7L{Q4GcZBkw+7# zW&TL=p{&!dUu!y?igu`q`pUdKHa3X!icnm8=+H^i!=3ni zDe$QLZ6UdcSlM;bB&=We8&1AZI5>L34@Zl%z2=c2ytHfhB8O5__4r6}1*K}O0w9e) zbEY%us1XFca*$73TieWqPu%p6o<()RJ1f#?!rca3bNSgkYFnzJ0tzv?_8oVx59|$u zdr@?ia2Eu6gtuTZYgTu(Pfn928SY)Qx1iDky!`&D#XS_yfNv=2ta{1UVCCy;)rC#H>d{`G=14noF@UDSB)kw&KN+_n0hdVG#5S!U! zvqm{gcd)m=z%_}AxM{OymIfNrfe^(!keOe;{b=REV1lX`Sm57RL0HKS_%VoKh~AV% zbLIr|x`St1PMXva>h^zSue9|Z3l{V_uB4cVLVmAV;3mFf$hm>?Z5bwDmX-$|4dEW| zS9561BHc*0j16k#8)*GCfY(!omJ-txhXNbwXHJ`Dt`O62$=saR3X^M^V)}Ov%JRs|WbJ<`8df zN_8~_l%Q_dNt)0xp=(;KmAIbn@#>W;hps1n4lJ#ofgSc(;!xAo;$uivInK@Ps-J6? zFZd8l!wzIqp>x3xSgfV4rw6pT1jN%VW7?W!Uk4B&HJNRQzNjMiv;O_0V-u*PUULkD zy;2Ju4eC^sx%NJi?Hx+HSD@YQ!FGi0l=st&TIogGSX&p$=k%;?7{0UlrNx1AN4ypqdJfUk6cfDtbd!z&+Xi*;UL#8v`g~q-GlVTbLtQ9 zkG*~_X*Xsvy$tFI7n-H!3c*ycTpde1sW8d3NzKgM0&?8XAqDZ5XtX$SQE%H%po^|U zP@eVu(Tl#(`p7OhJ$BaC8wM?We1Dp4KuT+BMn;V?YKJs7ZQY1oQFN}wkf`h_hzAZF zD9(}*3-#Tt>2EDK@%y=bZ4@%x#eaAMIL0ZJ_mdxYyN%F9o^G#Ry*A{oKe7>%7$@w_ zL`%oJc#T%WcG#+)+u`eB0}#hZ$CGciMG$>g<=j1cmdVI3K7Ir)WtqU|K?5{*{cQ6Z zQwf}(B9Os6PH?z|^Ms7$JsDMoFDe|U3pSEEXOK8w|r;C|$?)o~I%>(<9%|sJshx>nvdh5Lt z)9w$^L=M5yXpY)nwDG6u_ww2s}czEY4J)&pNoau;;tnl8wEc+8y4arj74J@9$4j64(5>_#b8DhB2g~;Q_Vd_Ksg!HLLD}hMEUp zgX4q=mjMwppXcO#PUN#7#_dl?XbRp^X|h{iOoJ}2Zz1+52tU?vl7ye;9-MD?0c5P| z*@Y&!0mbtMfCt1(eR0g;USR$A9<(|1J?V#2orm1}auGIfG-!()T`6&S9wLz}EjJpV zi;>lh-9zCiSV2ravUE9YsV_;jtyUTq4zLG3@_>N+4U_RtFSboBNj*$yC{B14WvRi& zcgRu9oPa&vbV`35Pt+-{<$#qd({A{bK?5VMp2W++*hLpqOPcpmX%yqebVsD>4BUl1sYxBgKPtUh(S? zQ3BM!$p7lBz8OKwR;9aTg@LsT?kRDYq>ST+1yg#pMaFOvaUMQYTs@y0`-l1zYGLr) zPq}`?v9bHZPjBhT4$$&@0qd7Hyj2Sz&;qJD5^nm2?7OU@mj5tA|e>v1G;7CEQRIyh5XJLulHaAA#T zb}El^q7%g8ZaM_h2hG~N2EFW4Y@F2h#nq9>=g3MD=x2`K@NVxz`Y$bRgYS;!hgaNv z56+RhG=LG>svqwMAD*1k4i?5ACEF;AIR;{=Nz4tVs|~HD7!q$t+6LKF5}MpDA)b$N zoH$XOr4!uxHW3>LIsq6Z$CoK7Ew8NoIoi(dJV#I!L=Tu{KNTSd%6b{5*PTceBL5`X z!CGnGZxO6<*lpud>)>(qgrC=K#Xoij9@oMND4+<=@Vuj6)mzEAN2Uz?C~rhKKSw^X+A1B=spRsMo)pa#!0q6eU$DP|#oou%bEiALko4+H*ePD)DZ9Tq#e zCW$PRIk(tCe->rA;GdK@!uy0BQ(jT=`RRhXx;h}yMhXnmnyH2Mi@;e<4L2wd+0c_+ zDFvyy#3+}VngZ|u|C-tVue}pujL#S-_Khi_UC-FqC1utiKTtl1qUsF|MWZZ-o53Fw zZ3&q3%iN>)@83st*O&u%ajaS0dhUWu>3P#OKq0tvf-(wVdi}Q=sQdmu-k+@QJ295N zMYU_!e!DKtiryO7C}X1nT>G1E$c-NZ+sRhMDg$+2jJ z0EU}BTh}B6W#T%8BNEEHHI;GPX)Ca;t;>gxv8QWjv=7?sse2DxZ`|Z&!42KKOmiz^ z>Z;SNhkK=5-`KrtuZbl>Dni*4WnFx1<$cQ99{msO?=3A`X=!^J98uD~8s26<6K)ML zuLEntCD5RM!AWp5g|?AV#^sl=)$+BTcOACOaYg`2!OXv#oPX5LA3Wrs+iaJlrZ6_P|I90@il}8B^do!^b_dp&5;y}4>tmf+ zQ^k(%x7+n`1F^K6kiT)LiOF=Zg61t-UWDq}tpw0<8k4&1#&c9?=<|zGTL&S=BW%I} z(ykiV4>>Ka2f>UTutxzI?Y?&il&y=Ur4v(r(7dWJCQ6QdaF^6d|5OgsXm|cTgO@Mi zQd7E2nDHH(rbY^WVe0syBnrCZt#^ykjsOe@!-MfgT=jRHcn_tda2ncwEHiKS-dz`^ zM{CAo+r|}Xgjz`!=F?(br-05-^60?u@LA5XiZO~Sd3iVts{PiinTy2l`}emqo_)>d z3Z0CK zk23*Wkg@d2hvZzmyQLC#a*7^zH3uOi0{@XepDzMq)9BKr@012Ykucr)OKK3lNgK83 z|L@4oYEh5DsUP+>UQL8W`@OQlcVk%CrJ5xQZgt7ZoTGOOn8k}3H(cU_ZhgO;F#fk- z@3>JL_s7Mx(H|`foG1ddko#^GwJb^|;Uv^Tgb981{%t(il1AyhoTZCo)W~&)ovp8( zFH2w~cTX~#?~*H$D&f z_!De8Lc3+jc*EViMtTVE>YR!SA-#M;d?j?FPoF-SdwL2>YrrRX&tKm^Y7b14&AjPw z%&WhSo&HgqE~P&;Ep6P@111~1kr0tFd^_sJM2j;kUre9WMKY`8YQiLG0q5T3u``#A zotX7`hjr$$W1EANJ6d4phVPs~WY12Sto#y{1nfXxt&uTpQQCmjc4vzF5O`wOlL;3{ zlj0g8O(#rBJkAP_ulRm_ejH7Quvo#SLvpMJT2nHL(HUkz?$^#;KXZU?k6D~dVkz=^ z9QU;A@W8NNza(mTX+eYvGdu4q$f>_q??&IdJMTYP_X25zP_<(`GP6PatqIr%!C8)e zf7j_($M@gPms@54eeiA6_OSv$iLetIsonVTTZv(_OnQ{??iD_G@ZBS>WJb(YbPu9deJ8s568-Q4U_r!v~_za;q zAK5YD?HdkH*Of+wWuty9rPnK_r?BU1%a1c%b>E?d>Jf!M%L~CW(xrm6JDfM5zMzOfhOKMcG?Y>S&x-N{2zX>s#ID` zg4Ymz^78X#iOg!??NX1D{2--b-MXYRXNEmw%??r^p$`Bu6obKP-ZffGxq)86l3$@7 z+2%VPl6~e#81kMFCMAbrhkbxbS4D63OhdMQiB~#Iec9&H*c`M@B!BRG`)UpKU5}7+ zr+YyI(-KT5<3(XWJqeMpjeQzs8WZ5+&A3LTFO!q5d0tqV28GRTb zc~iy|lKkKs(L-NfiPj|BySy=n^*7QTdmYt54=!+C&%Z2vH^#e$zOGeC% z`4X~T0zHoS&jYe(>oN++<9HK&1V-cqe?I7XNN2CPw?e;8=I#k@P1UoS8bA-9UqRd< z)PflD`5WO6#}jL+(>I2EqAA89T(ERea`CbOs0bEIg1~1?nZ}jz!-TY}jCs5NSFb<4 z_Umie^pf*#z6H@v0=+1O$P*PaU53X+I~rUPYkjcNje(TwvZ(<0`~0n2suaMxFH!pl z3jJTX7eg4a#DONHQ_uXknS#FP0sJ4%3fB;wwg>-<^p=!B8VE-xC!MDg;H}85reNm~?W5kM>#90;?`8Hcxh;4B&^EQQ&(kK7`FK@X zXXohXxSCQNpkGWR>1}0f!j(z&G5hA-Ter-a>2%+`syb4r6{8P@y^|rurcDkDl$uPu zW>sq{!v}cTIMPK&MQPXnGnx%;APdwD-5e6$ELsW*E7m|%2R0lWtbx{(!bH8r#GU{WP_8KXoR!%JiR=4V66tvDB%=$&rL#!$YyI8Y!9G0j?DX`c}GiiCxlupQV5 zhc9p+&AWRr?Ej}vzp!Dn8x{o0T_S6eug@~eF#h5JB(m)D^A_aFPqO4IFVE6{)EbWt zA7CO+L;aCuzh9wqJPSi4TyZ&oHTMDa*-LGXoxC8pM6_P1U*Im0SNK_V0r zDcS5w567V30#n*kDjPjCTY<&Xxm6yYI%rzxxHaR-{v@aLV*LB8ynGFcIQaq_CNRUG z74@K8gK`u%UTNiV`w{rDs8jXXz{h-6S-FnL>8qulXV5#^?&|}4jd$1e^T!8#6g{!5 z{^FvNgI9EOQoOr8B_2aBN%^wNlyukxgH1dpH@wa0kXYyunyGf^?9zv@f_&cM5FYpI z-%bn7j5abdl2R}~%>}@u>TqM_yesWSB}9Z1PMLA2VC^aK0yGfo8kJIy{-Y0ER`=8c zP5T{p3HNO8MqpM@=Py#v|N7$V0rF&Vs&k*1?ze5O-MiOTstZVJRo4fXO=ARubLu}P zy@Sd}!|{SLx{hyRpXJVtKJ}(V8Y3Sj5SW}+f~x0oK98QO^* zGueKktSvE_wf*LKb;BmSG-xICw6zuF0&dCLxVWxz!*Xd_3)A|>+I2jNl2+pBH_#;` zxXbeU&=Sj~`-y`B-J3KEeXEaJUA0?w(vKWGS#AX=g#4MT34~bmEAhx|xgOu-nC{O1 zf#N_K-;BUZ;4r8OKc>eZ)DK9^f1>P57_$ZE-H71a=cBoB$#Cd)m=ieBLj zVN8bY1{z^)YAK9loo8ZV5UjURcNhP^RQbe9rdpgB9;zjPLw|};;@G?6HKCrz2Ce2; zV;CVTJY+-$gz)q507`^p?46`^VB9y5)1!+=-_Z; z&igiZ5|;Bc?oMJLbUn^2jza9z>C?`1<$Fxo$7uzpO`~BIo){2|xQ(_}R{w*htw8g0 z?BvPz4W>&!xQAMew37W2p;m5FrpO>F2{3EpHhhWAb%Xefkigc>n`NaYyo32F9SajHvEz})Mvu@`L944fWJm{@6C-?AU{bCs zrlve(tw^OjMepbVacSIOA>`G6iEDVq+fl%@=ou@Vz;Ifx`iEh&!m)D7Oh5JC6y#V$ zlu86cvCW5uz7{>RNx@0*-0YkjDHa)!$|@?6`r&%3a7BvXmO0nyrSFcQ84O6h{rvKA zG|TtweE3HjmwuG42vjbD|9*o14}9anThsON+1fSWsna6-0HYO8^_kuND@!vW>8uY6 zyE-R-5J;2k1BG`YJBCqRRl#uwv~8m30*F;<>@{+JemN^kKxqeuq&DYlCd&kaQj^KW z8Y=28Fcd#7K>h2+V7HP@3P33u{l)?}gWt>E11@PgQPz>x=Sb30lWjby_``=s?|0YT zY;XjhUF>I}Fo;i_y4MEGgS?grQ!xPCP5&?QQw=j!guKv) zWvwob_*y`4;%26%#_^MVc#ZafDBOlh<0%%BOlTmwF-hw!3wYV4h3!N*;Ymd(Z$lfmIxDN% zHtwDAwHyYb*(&8RuDY|H%XVyW;1a5)P>hkn06tf+5490uCo6h$y#KEI8FbB8H`JR* z8X`;Tph%~DzR#9``&#<{#LJEZC3183 zT8+V0?>yS%or3QmD>i6IFEEp3H?z)ey)p6e1_H4fd+Ie_@=MP_7e*o&fa3Jc6t69WS4a=PfS=# zZc0QJUI)vwMx=k)V*L3y91wQ>_-fhY$_Kbg0Ahqr3%iaY>UfE#o~4y5vwj86H7po4?>Yc|3{hkYBa%Z6+u7KSf^Rh-sl)oQ6=`(stxA`OBDnT@wsz zVtLmIMqv@%oo#e-ADQ$qG?aqSh3MEa?X=$;LJ6B9_DP(?zKX2>QGy(K>y(ORA`e_+ zFYgTRdJvuWfy0NDvARO2yzbJ>W#Ktc+zsH@SROTh{`(~?{N z=IEB#SY7&GqBM}Ag@;Fc_EvYUj7FpC1S5Dy7ne2->D27R-MRDUTiV(-qqB34JmN-= zhq0oqPhPu5Dec;&}! zHnz5sX1|p+)K7eC!ka1k_=s*gLx*-0?-iO>Fm_aWvN)c8m_p5d!8rysnORx!NlC#R zii@n_re{aUW&_@K3#?G4ef%50zCUE?0$@kkO8b5qE%PTAiu#Na*n>^qB7CbcHPrmu z^X5Q%1~(z$;g^JzPr8-R3v@|S1+p5oY{pAQ&D)3g?7#F9d*Lb;u?yBkg#$++AD|4h zu@Uvmnx!#rKzAN<=QbZcd^lig$X_wS>AQP(J)-2M+Nh+W$G_Nn3$T>^ zRtAfcc6wvXzxd8z469#%m{N=NYaX*_D}noPm_@aTDt_6bUc<>&Oo^h0wBsaDln5VC z79Wd4qKMaAKGZfPY$<=40I&cb!J9dQYztu6Sutt>dC}|9iB=TCl0blKLtv}@a(R?x z6L9U=gm~fcCH~zv3fP%}T7_HOvUz;!&Rrq`_KBX*B_|nC&R(uBCp! z#=j-VeU&Vvd};Fd<^*TwwVdaT=Q@*;RznqmjXPr0sNcD9NH*5ft~`G7gw^XO$3C69 za6fUfgF;Xv8i4=O_9Rx4+zQUGX#(pcgs>zFBY-4cn2g)evu|G;rs^O>!H_x7?m*(y z0IKL;ldHf>hpnzaXDU46_XY1%Ej{jJdg#}V5 z4SWS5!p_caH7Eo27VA+k{qdq-7gAWF5I<#GC&Nv&3SohDZ>P%IBOXC~dio9az8ErW zhLd!W_$ND`rA?#_-9W)q8eiCnoLc#>_+Dc;z@J*nyD11B`jy{;H;W+FE-)cM4b;(984BK zbI$IKbzI!YGoK%JI7%oi{zePYpJqr-DME&exXkUW?KF5df$Abmi44S{fI#Od=&=z) zdGq_b+j~7bg9bOIK^A%}{WwWxn*Ej;EWi?kmOQ}CZ|#q~YCDtXX2C||X8uae5JGxQ zID9_AK1OgC5+k(IJ#RC%F9o@L#zP%~<{KttBqb;Y1Ho@I62c}wG;TQxqi4|RdTHz>1-*hgi`Gu+^rRj%7N`6&E+~!GL(P(7zyI-qowXL3J_Fwn?RYx z5;OT$W!s-!|Bl?^B(-xMb1H98saUJFqX~LKv(jkaeg^10(Y4$pVFky`X$bc|pSL7~ zQc1qsfMm;^P5KSKR6B3f1Rv0K9U~)E1}3gKf2WOp6V-z*W&>KRAgUnQr^7q7ZPIWq z^yl-=)?w~b7ZeU))48#1FJ%UMnb&@%bMTprl?mY%4}g7F)xPdA40Yl0#2beqpaj$L zYqKY;Z(e?3&e?~XZQZTbqusTbJXwo_&y}bQYR-=&#bs|=>oE`Wm`T#Qfq2byUIkxI z22K?6+js3^>AMnhT#mwda)(3_q45Y!hgMEUwYfsDQK5*_ZAjrw+Z-C5z=U3F4 zDk%x2j#$<;tsnI-Ww40GFeN_5>`EBTsK|S}u zwuKj8z$;rt4~g+iSj|gIANx+!PfRP1V*beU9q;s`K4^6Zn-P!2B;64P(X}@)6x)s2 zZnQ>m3s}m~$S4ZFn;nJ|x{4)e8nJWHQ{VtNJlP}e#?VP+>je_ueWbWznKm&s0$=U+Z~2TWmL9JLZ$xYi zGejm~Vx7`xAGLDe*CAI%E?+(rbHmNDop{GRof%W)^_LicNl(8oD})uNfWa{Gj2GhK z3eX5va_CJF1Va$dMV(YtwWGI_l7ETstN%j;YACKv*~Vzp;fxpew&`lpSGOrp%6c|7 z$FHbjR$SN|ScZRGtuH-SZw=!jZkWok3Ph_67d9^+KA<8$amH*78#YLN+wA%@rQaSh zL2HHVvibD)mO4s$Nj>B=#cF0=LiZ+PD-?l&fyfmU3g%KHtm58%#v~ANIwf#^QHt$> zk56(=E}KMqq)@~sUX0#rj#Pqf5vheJ5d=G6^ZO9OSr z)CF`tU)HLjZTX3oi38KLOMhA>zA_&*F6`PraeXx+5;ZnIv~KEjYtP^*!`hFSe*a9! zl~BtS^U_^)N6ox*UgPIr%UhvaV>6v=3T(BD*v*H zU2hI8TWnf($l$}$6E|FfcXhm3I_%txUcL($i7Qibtm;}Y^-$F2eYi;|PCklYVEN6j z^G)4_jOb(*gvnp_D5M#)js#F{zj^z%imwfG*dlF0C1}O3>D81oSJIjtyf?4>@0{XC zk2a#VbWNME{rYS+UMzf4MTk*Sc$@5b?7CMuFt_Qy?weNr`^ccR;=|$kj|D!b+@~xW zkhZO;YwWgd#igaqPPu>BR=H@S`PijupT`nEkCQFWZR!uD56(@SmK)yD$-W~fLs*!h ztmxIR-yDcDQ-1up$6>Nuog~$<~@{ zH(#>5%zD9ByHBTR(BJ+2C&lPuI_IOC=P3E?`4b5gKvr7WbrtO$9t!T{}Yk3ykn%Sns zioCo$8P0;hQj}}Q6gJih{TY!zeFRTtiHX+EUAuf3v6$OF94VI@$0E#6pNv=mK%8cz zuxG)_AE92O(3?(rvlA?|b_Y)s6Yw5!w2-Wn_0cmH6VhB>`{D9K3dw%I;fAAm%(I^=N2#v*x?Y?~Ju`{bjpz`qt;nb*v_Y3d;w(Ps@!0FSPjLN3`elI$rRqNIVcwT$< z>=AJhb(%K}{oT+aZWT9P^OPUnd+*-(xdSy1jqBM#SqW*O)P$fVB=CbZHKkQwUXE2( zQL*4%_>a;Du{(&{YKwSt$gunLl+@cgf1dM~tLtjFw9B+w{VDq?!+^@U4!H#cm4?MT zq8*9h%?}iZpln&PbmE$=71VB(d~QoSOab|+7-!PMaA@mYGxH1S{mxS z^~rexVJZ*xTDXm#42{7ANFRD=m(VoWjU8J6W}>}yB&@Bo=C)5WKbD=#^BR;lbAZQ^ zB^vCK=ZDwjm7Ak_VviFO5%zVF5Z*F}2`^*RMU(UoAVkU{EK;H1JgE z3aoV3Sm|D%+SP8iQH|=E47MQm64`(RTq)utj z5SiP~oo}*EXgtv9nXvv_1#mPC-REMo=7zFaKyA}q*DtpkFfYsZ5yQ+EEvGQp z!%;p0c7(U}eU~UQ0nwQ2f%zx}UbNl%45K%(ujh=jYoT+=_HJFMs)fJxhAsTB)vy26 lP-CYv=4)31(&YU?rz0%X$ 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 Lt4LKz7zh3j9>y&v diff --git a/examples/test_units.png b/examples/test_units.png index a3b579c23ca8a8c952d048d546566136d3e5b128..7f2e461fa43e3408fa6a6f08afdc45a40b34019a 100644 GIT binary patch literal 9632 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 literal 9846 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

gvs0x$ML&SZSLQNDbp9dA zIfFDsWpNWk&IG?0EIiP!EE$ZOUKR3z(op+1rmFX4pz+so*{tOA$cvu~sxnc|f)LKMYg75+b%fQpjc>V`R72W92W>P5E6% zc@;wb{GHKmj~#2=yVX5-aIa?Akz}WzR?c`!CFm(>MNa-n)!gFdHx#qSaJZ^^a0DOf z?|GDg-(f3$eL?}+&<07Q0v_SaIT?(x{z*wSvZKX0uLWl;f%7$&of?Td5duYNAaCB@l8 zl6gKAuMDvusA1H+kNBMk!*0!dq?WSG@nfI?V^VK-54^&zzaAgUs8u^ zY4btzbS@Gy6DbHbX^?J&YgD*g@ET_y9ys3TrYakUkKmTa~a(rHHwso)~BNq1-Tug6HYpEPL@6_H1&qokTd-mb>L+$vB; z-US#94I@`$D+EJ?ja1N!t3M2l6NgUu`8l*FDnEGBCMsV+6Jpc&q}j(om_2K1^CdD&LqX!J^>oyH_xTik9s%e~U~&v31mKi;9yeUOPISr&rh ze;(yDKxgWjrE2&bV`@1LVYe=b;VH*8a_+t#Pt7ckt@$4Q^au7!>!#%6+bl(@jt2+) zB{sGucsIEm%^SXS0YK(=)dOD25~l&@&*8SJb+1(xZ+)6lD@sgp?lH+Z`mOur44E1V zo~Ph967ur*o)_ue6;x@JQ^N$0_3?a}^~2@%KH#HTfe7+)w03yMl_vkTb$Pgd~y>r1=eUj+tR{2_q=#@SpH134qU$y z!O0EKj-ArwSGJITKYQN&_uOkFo>H5q1LY6b8%&pkknL9+Rxih#sNxN>YJ>@P{~%9&hK7)w9-`31}HZZ zSh6H}LneLfHj^aTUT+{9S^>A91%TMXp_p*Rd$cr$7YzyKC`d`V%e~F!EQ;<72mu~9 zhf6gkA#K*a3`Dfe0Off)%kew6BRnHX=B#J;B;ot?y_inO;teM;(IVgtZiC6IH8j8< z_m(1ziPBSzKMp<;hz54X;sa~fx$pLJyR^haX3utog8(Osq>SmY)j`Z;z$Zq+W*Y3e z8M@2s6}PpQ;ZV))elW>x;M`WE*ZRq9D=ec+L`LZw(_nCDP2(4v=EeoN(W`!AD%t_| zR>l+RiFJGfM>D=%a3i~;F$sdCKvXgoGF7zK))zz~tWSsDpg)#3$O_@@@1gpkI$;q< zC(z@4>s!E{dqs+A$GF(TkpNdPX3G}(d)n~sGE#LgnwWPoPi~5PPqJppIEI0uAPEm= zhw%oRMR| z%U$WR*?kA>lUnl%jiQ8l((juTwpja3KS1KMa-v3o@v4#SkR34;M(xU~(|>j%mYGx! zv^2cNM1(fd;he}!eyXndl?TqT688^}x3ayMrQokUHpGMH-*!ta{ zJ4s{WSc0pd3GMI@qfz_E8o7OP^NFV%U9}PZigJl88^+)`O?;Z^9Xw0&M8!?Px*Ie0 zJ)&7xjPjidG$e@3{oGbag?~UZ!xX4}{q&s%79CYhw%>w&#|r~b<)(f!t z@iM1q0H~PiWh52V2)42^VEQ5{W|@Myir4R~DQ$nw4^62s?0F;=Nl#MFt~XyH`Dt#A z9`B=m<4Dw#r04tlDjliV%zWvGX|MO$UGuAqk4Gv&<(W$m*2{QQ_~-2z%R?g5l%yfw z_&WY!MVJrBo8kGjdYwLX;tm6nZn&2Y&A)9|so0_dzmsWU8BogL#u`ZF7}(NwS;XjT zuhmZUvWuzcHv`_| z?G;EAb;%Nog5ST;$!!inq6-I0OUuGsGtx`~+l=K&=OW}`&X8)av8;qjHozj^^?;LonVd@U-1Ws=X!!P|c$A*c-Ntx0fEtNJTsso(8?`Zp9Ria0QN%P324^D~LKh3w1qQf}FC#yc|bZzU)-ED9h)ZbKr zNmVj;1jn#7yqEJK#Ff)T=o2eC6=chriFU;V`Zj~Y)UaUvdwuBqdkk2g!79|5$PV6sDTB4 z(08jLTET=^tJm)LZ0KL#%M4y>ktmghJOY9#U#mGwl#&B#&FjOks5S=ML)9JdxQA;; zla$(Kt{nf{)eSw&CFkjI@c0A{?wIY^wylUxhbA)KiX zmyV%q?~p(NJrqtPZ~7K^B8Xs9Qu@uw46nAoU%dXk(3#=a`G(0B$!EJHqX4@{*F7*! zo9Q^aW?6nmN=;(`=Y~4ciVmxR>_LU3HK0}VG(8k|2cQoJNVTsmHemJ?MOGr}7)&S3 z51+@JfX{8*yCifZ4|4Qjr;DB zL?x0TKigyg$r4dx*@JjDdI;G5q&By7h?hGCTRpAYlEjnqF%Svl;jI`dpIvKcHhW7QYK4)woZ_dfgmGl`|U zQMxrBA#~tW0$aZ6L)N1;4>RcYa7Hp6yfORQo(p3`F?oWMinu^Po?(r4HHrmW3nz@a}oV!j7nUaZu!Ktv}gEm*$X z?mq*Q2NK9*kwb*wrH)p}e73%5&rhFz4Ez)I8wVtd@@UF46J2Z6;kAHuR^4%~fOzDd z^R75mqeNNjXANY!A)hhTGZHl3NyX6~G#gbOxZ~8aUYt4GHwo@3!Ex**w3-%HEq96G}y3@B5uR)-#Y964*2cbA993l$6BNLFq+ zJh`e2T4KTGWQwP_Tj3b5o$M{((X??o3l(ZKB+fC%Uyqo1;VX;xM8|fv_GkxRc4g^5 zbEb3xbpoQ;HHl5@#O2b)HyP@dxQk^Vb&dn5)3~_5J{}N1kI>j96=~!C%nJU7Go8h= zj}%^NL?fY7%HRo$j)_kEc{HA``Ku59t3z05oPr#x6fId_7#c1&=b$?g*8$7*`BG6uOFAd@`rOWIVbZ7A+BDvG>JB z`@R^_F_b%ObYthqYKx2Mk81X;bHD1MPpy0ef3dXa-3PE_Z3Q5sOgwh6yQq!{H^&QO z;m)NQjqaPA0lQ-KV6h`8wgyDfY;u^>GPWJlJjFGb3U0Q0r!}2uqB<){BWslUMyHQx zhTIQH)7V(oP+qfz@{#0CANi{skzot5$|V|3-!JPmSeFqc$}PcEA;n&d%xmmM{uB^q z8kUsg<90i&!9~wkb!MNo0HYF`54NWo)tY|U*MKngvH_KYF@8>dOb4|zB0;apA*}r+ zJ`VGf1}UJ zRX4fqDLBE+hR~_Y&R#RU{@EQyd`M%~Gap>3rEx+uUoqH?*t2l@#kB>Lf9_WzMvb-!frzh#rT{7RC1YiH=I#|_*O1Zw14amM@am$yeV-~d zna(M@^a<2MQriQ@A}LOM5bXCL)nF_BibD%h&7@_m?g(>jXY+PK0&@{j7nJFDqhwO{ z%3)|oGFz|@ru5eq0|*qo$T*!gUyr&VP5BFZ1)}NemkTxT*&Tc}8xp(64iI5>Y1_P>60O0WVYe7M#D{JM z{krHM`Ue!ADi+%!iPZ>f<~!8+bWQr!2E9lVj`?XnF!inkPZBBKpkx|Mve<;d?SOtY`2*@LGA`~ENhnuyVn_8r*1!L_k z+vIA=)4C#}5`4d-?f=uy*T22S`wi9%<`WPGm#&#d4}-S`maC)?*-_1##mHBi9Nwd| zwt5#>=@R^=GM&fUm6PuU_^+)cV;00hENHIg_wm<1WnsL#K1Nk@x3fBM`d`m

_*%*m84g-m`r(AGO{_M{om zzsx9O*{m?ZnGKTyiv7~V^P~7-Ig&UCP4&X6m>;VV;W(!GEaF-03M)wTOmy}7;n3DX zb$s`9gMcj1v~8v>4)SDDaDM{959|Z$o!cFvHWGy-UwP`Pho$gKtw2T(T?MA- z)A3kSP|UWNxlbSbd)@RaJF?nq%C8U#wCRx%n>WF@37Tq7x5+2e)`(fKVj#ggD~tAD zeRZRb@n@@ec+`7}m}Ktk?zEX-Ic}p2eTy=f>|w++#WP%qzq%tR{ZhV<>~E_rGXB6V zm;U;cYVe>aSxx{Ep-jdMW9(&W!u<93V!lj+P56U|y7;0IU&EG>YoNlmM>-24qN_Z( zld@~zS_wBioxj0xxn4|x2(LtwK*JsGJ%}ZVPo9#xG6CndUE5+MgMFl0+H#TF)_-do zX95M|s2J)iNi&shuDW-nBqxGr`ijjv9dtjbhZ-5V-aa)r07ngOF@Cz2_eUZ`S$lV`kP(pFQJ6%z9`KPK3jT&L5fy^Q8UNHY$mKE@24Zwt!P zj@f`>1vyf}{gy+uKiRF{ak|xMwHZOwoP=#K?1OLQL%sEvb4opw@7rIi>1i=qWfi4( z)))L7>KRIq{AfBnSRM@~1M=)BBOBYd$N9M~Xc=FLr2E4f{Hd^-^Xskob%tsE(J3>D zJ}8yRP3A>lN;lC-QR+-(Z<~xuTH$V?l1_e@vG1$aR=gbdE3ZV>H&k#bm~hW=Hr|r3?a(MfbMiY`QK;%3NCN@N!I|B*y*OepOF>|S8m4_Ke$Ot6}#5!#lloIh<|lLmysOD8cAnw0MtagDV2*Uj8&pvNM$r^ zcmvKf(Ri;vSLQz@Be=giWXPYMKpHxnm?6TLC#dXyg@pMy;N))Ma#|)~-@W*aN;fBaX&n-DHDQ}>> t2=`{w4<%V1cmpf!QQ2$U(qx!_h>oBJO)4B|(zkEE&E~9 diff --git a/poetry.lock b/poetry.lock index 278277a..7eb7229 100644 --- a/poetry.lock +++ b/poetry.lock @@ -30,6 +30,32 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "apscheduler" +version = "3.8.1" +description = "In-process task scheduler with Cron-like capabilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.dependencies] +pytz = "*" +six = ">=1.4.0" +tzlocal = ">=2.0,<3.0.0 || >=4.0.0" + +[package.extras] +asyncio = ["trollius"] +doc = ["sphinx", "sphinx-rtd-theme"] +gevent = ["gevent"] +mongodb = ["pymongo (>=3.0)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=0.8)"] +testing = ["pytest (<6)", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] + [[package]] name = "argcomplete" version = "1.12.3" @@ -141,6 +167,14 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "bidict" +version = "0.21.4" +description = "The bidirectional mapping library for Python." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "black" version = "20.8b1" @@ -176,6 +210,37 @@ packaging = "*" six = ">=1.9.0" webencodings = "*" +[[package]] +name = "boba" +version = "1.1.2" +description = "Author and execute multiverse analysis" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Click = ">=6.0" +dataclasses = ">=0.6" +pandas = ">=1.0.1" + +[[package]] +name = "boba-visualizer" +version = "1.1.1" +description = "Visualize multiverse outcomes" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +apscheduler = ">=3.7.0" +boba = ">=1.1.1" +Click = ">=7.0" +flask = ">=1.1.1" +flask-socketio = ">=5.0.0" +pandas = ">=1.0.1" +scikit-learn = ">=0.24.1" +scipy = ">=1.4.1" + [[package]] name = "brotli" version = "1.0.9" @@ -330,6 +395,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "dataclasses" +version = "0.6" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "debugpy" version = "1.4.3" @@ -417,6 +490,18 @@ python-versions = "*" brotli = "*" flask = "*" +[[package]] +name = "flask-socketio" +version = "5.1.1" +description = "Socket.IO integration for Flask applications" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Flask = ">=0.9" +python-socketio = ">=5.0.2" + [[package]] name = "future" version = "0.18.2" @@ -439,7 +524,7 @@ numpy = ">=1.7,<1.19.4 || >1.19.4" [[package]] name = "h5py" -version = "3.4.0" +version = "3.3.0" description = "Read and write HDF5 files from Python" category = "main" optional = false @@ -1261,6 +1346,34 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "python-engineio" +version = "4.3.0" +description = "Engine.IO server and client for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +asyncio_client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] + +[[package]] +name = "python-socketio" +version = "5.5.0" +description = "Socket.IO server and client for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +bidict = ">=0.21.0" +python-engineio = ">=4.3.0" + +[package.extras] +asyncio_client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] + [[package]] name = "pytz" version = "2021.1" @@ -1269,6 +1382,18 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pytz-deprecation-shim" +version = "0.1.0.post0" +description = "Shims to make deprecation of pytz easier" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version >= \"3.6\" and python_version < \"3.9\""} +tzdata = {version = "*", markers = "python_version >= \"3.6\""} + [[package]] name = "pywin32" version = "301" @@ -1392,6 +1517,26 @@ numpy = ["pandas"] pandas = ["numpy", "pandas"] test = ["pytest"] +[[package]] +name = "scikit-learn" +version = "1.0.1" +description = "A set of python modules for machine learning and data mining" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +joblib = ">=0.11" +numpy = ">=1.14.6" +scipy = ">=1.1.0" +threadpoolctl = ">=2.0.0" + +[package.extras] +benchmark = ["matplotlib (>=2.2.3)", "pandas (>=0.25.0)", "memory-profiler (>=0.57.0)"] +docs = ["matplotlib (>=2.2.3)", "scikit-image (>=0.14.5)", "pandas (>=0.25.0)", "seaborn (>=0.9.0)", "memory-profiler (>=0.57.0)", "sphinx (>=4.0.1)", "sphinx-gallery (>=0.7.0)", "numpydoc (>=1.0.0)", "Pillow (>=7.1.2)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=2.2.3)", "scikit-image (>=0.14.5)", "pandas (>=0.25.0)", "seaborn (>=0.9.0)"] +tests = ["matplotlib (>=2.2.3)", "scikit-image (>=0.14.5)", "pandas (>=0.25.0)", "pytest (>=5.0.1)", "pytest-cov (>=2.9.0)", "flake8 (>=3.8.2)", "black (>=21.6b0)", "mypy (>=0.770)", "pyamg (>=4.0.0)"] + [[package]] name = "scipy" version = "1.6.1" @@ -1634,6 +1779,14 @@ python-versions = ">= 3.5" [package.extras] test = ["pytest", "pathlib2"] +[[package]] +name = "threadpoolctl" +version = "3.0.0" +description = "threadpoolctl" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "toml" version = "0.10.2" @@ -1695,7 +1848,7 @@ python-versions = ">=2" [[package]] name = "tzlocal" -version = "3.0" +version = "4.1" description = "tzinfo object for the local timezone" category = "main" optional = false @@ -1703,9 +1856,11 @@ python-versions = ">=3.6" [package.dependencies] "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +pytz-deprecation-shim = "*" tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] +devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"] [[package]] @@ -1791,7 +1946,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7.11" -content-hash = "68863d7ef6290b0f7cf8d36bbc34cc1a15c20ac4789ff238d3e1b31f824108b9" +content-hash = "404c4277ff12bfa10736f13bb1144d8a33f2649daefa26f2845ed7dac2f4d1f4" [metadata.files] alabaster = [ @@ -1810,6 +1965,10 @@ appnope = [ {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] +apscheduler = [ + {file = "APScheduler-3.8.1-py2.py3-none-any.whl", hash = "sha256:c22cb14b411a31435eb2c530dfbbec948ac63015b517087c7978adb61b574865"}, + {file = "APScheduler-3.8.1.tar.gz", hash = "sha256:5cf344ebcfbdaa48ae178c029c055cec7bc7a4a47c21e315e4d1f08bd35f2355"}, +] argcomplete = [ {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, @@ -1869,6 +2028,10 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, ] +bidict = [ + {file = "bidict-0.21.4-py3-none-any.whl", hash = "sha256:3ac67daa353ecf853a1df9d3e924f005e729227a60a8dbada31a4c31aba7f654"}, + {file = "bidict-0.21.4.tar.gz", hash = "sha256:42c84ffbe6f8de898af6073b4be9ea7ccedcd78d3474aa844c54e49d5a079f6f"}, +] black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] @@ -1876,6 +2039,14 @@ bleach = [ {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, ] +boba = [ + {file = "boba-1.1.2-py3-none-any.whl", hash = "sha256:8a0231dde4603fa535577e33d5af597fa33cca714c9b7d406a8e6274a6db7e35"}, + {file = "boba-1.1.2.tar.gz", hash = "sha256:9c17023fb56ecfb18d48390972dbabe20407819544bc82651fe4f7adeb174299"}, +] +boba-visualizer = [ + {file = "boba-visualizer-1.1.1.tar.gz", hash = "sha256:a711731b2fd182803f399d1787d2450d19de582bf518a1c237f0ee3d57d13d3f"}, + {file = "boba_visualizer-1.1.1-py3-none-any.whl", hash = "sha256:6726c10c8b042e6a5cbd9936f32267a71bfee93fc2d023bd9fb564cbe1cecddc"}, +] brotli = [ {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, @@ -1883,13 +2054,6 @@ brotli = [ {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, - {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"}, - {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, - {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, - {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, @@ -1898,31 +2062,23 @@ brotli = [ {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, - {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, - {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"}, {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, ] cached-property = [ @@ -2069,6 +2225,10 @@ dash-html-components = [ dash-table = [ {file = "dash_table-4.12.0.tar.gz", hash = "sha256:4c99689a887bfd035278b14ecd5db70706023389e53735d5e3cccc416facdbe4"}, ] +dataclasses = [ + {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, + {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, +] debugpy = [ {file = "debugpy-1.4.3-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:88b17d7c2130968f75bdc706a33f46a8a6bb90f09512ea3bd984659d446ee4f4"}, {file = "debugpy-1.4.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ded60b402f83df46dee3f25ae5851809937176afdafd3fdbaab60b633b77cad"}, @@ -2117,6 +2277,10 @@ flask-compress = [ {file = "Flask-Compress-1.10.1.tar.gz", hash = "sha256:28352387efbbe772cfb307570019f81957a13ff718d994a9125fa705efb73680"}, {file = "Flask_Compress-1.10.1-py3-none-any.whl", hash = "sha256:a6c2d1ff51771e9b39d7a612754f4cb4e8af20cebe16b02fd19d98d8dd6966e5"}, ] +flask-socketio = [ + {file = "Flask-SocketIO-5.1.1.tar.gz", hash = "sha256:1efdaacc7a26e94f2b197a80079b1058f6aa644a6094c0a322349e2b9c41f6b1"}, + {file = "Flask_SocketIO-5.1.1-py3-none-any.whl", hash = "sha256:07e1899e3b4851978b2ac8642080156c6294f8d0fc5212b4e4bcca713830306a"}, +] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] @@ -2125,16 +2289,16 @@ h5 = [ {file = "h5-0.4.1.tar.gz", hash = "sha256:1d159fb680f56260aa249a413a4d98dca12d3d9bce3b9fddb334df97d38e894b"}, ] h5py = [ - {file = "h5py-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aa511bd05a9174c3008becdc93bd5785e254d34a6ab5f0425e6b2fbbc88afa6d"}, - {file = "h5py-3.4.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:708ddff49af12c01d77e0f9782bb1a0364d96459ec0d1f85d90baea6d203764b"}, - {file = "h5py-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a545f09074546f73305e0db6d36aaf1fb6ea2fcf1add2ce306b9c7f78e55a"}, - {file = "h5py-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b0f002f5f341afe7d3d7e15198e80d9021da24a4d182d88068d79bfc91fba86"}, - {file = "h5py-3.4.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:46917f20021dde02865572a5fd2bb620945f7b7cd268bdc8e3f5720c32b38140"}, - {file = "h5py-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:8e809149f95d9a3a33b1279bfbf894c78635a5497e8d5ac37420fa5ec0cf4f29"}, - {file = "h5py-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8745e5159830d7975a9cf38690455f22601509cda04de29b7e88b3fbdc747611"}, - {file = "h5py-3.4.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb4ce46095e3b16c872aaf62adad33f40039fecae04674eb62c035386affcb91"}, - {file = "h5py-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:1edf33e722d47c6eb3878d51173b23dd848939f006f41b498bafceff87fb4cbd"}, - {file = "h5py-3.4.0.tar.gz", hash = "sha256:ee1c683d91ab010d5e85cb61e8f9e7ee0d8eab545bf3dd50a9618f1d0e8f615e"}, + {file = "h5py-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f3bba8ffddd1fd2bf06127c5ff7b73f022cc1c8b7164355ddc760dc3f8570136"}, + {file = "h5py-3.3.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baef1a2cdef287a83e7f95ce9e0f4d762a9852fe7117b471063442c78b973695"}, + {file = "h5py-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8e09b682e4059c8cd259ddcc34bee35d639b9170105efeeae6ad195e7c1cea7a"}, + {file = "h5py-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:89d7e10409b62fed81c571e35798763cb8375442b98f8ebfc52ba41ac019e081"}, + {file = "h5py-3.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7ca7d23ebbdd59a4be9b4820de52fe67adc74e6a44d5084881305461765aac47"}, + {file = "h5py-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e0ea3330bf136f8213e43db67448994046ce501585dddc7ea4e8ceef0ef1600c"}, + {file = "h5py-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:13355234c004ff8bd819f7d3420188aa1936b17d7f8470d622974a373421b7a5"}, + {file = "h5py-3.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:09e78cefdef0b7566ab66366c5c7d9984c7b23142245bd51b82b744ad1eebf65"}, + {file = "h5py-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:5e2f22e66a3fb1815405cfe5711670450c973b8552507c535a546a23a468af3d"}, + {file = "h5py-3.3.0.tar.gz", hash = "sha256:e0dac887d779929778b3cfd13309a939359cc9e74756fc09af7c527a82797186"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -2288,22 +2452,12 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2312,21 +2466,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2336,9 +2483,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2672,10 +2816,22 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] +python-engineio = [ + {file = "python-engineio-4.3.0.tar.gz", hash = "sha256:fed35eeacfa21f53f1fc05ef0cadd65a50780364da3a2be7650eb92f928fdb11"}, + {file = "python_engineio-4.3.0-py3-none-any.whl", hash = "sha256:ad06a975f7e14cb3bb7137cbf70fd883804484d29acd58004d1db1e2a7fc0ad3"}, +] +python-socketio = [ + {file = "python-socketio-5.5.0.tar.gz", hash = "sha256:ce972ea1b82aa1811fa10d30cf0d5c251b9a1558c3d66829b6fe70854bcccf0b"}, + {file = "python_socketio-5.5.0-py3-none-any.whl", hash = "sha256:ca28a0ff0ca5dd05ec5ba4ee2572fe06b96d6f0bc7df384d8b50fbbc06986134"}, +] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +pytz-deprecation-shim = [ + {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, + {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, +] pywin32 = [ {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, @@ -2732,24 +2888,32 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"}, {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"}, {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"}, {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"}, {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, @@ -2757,6 +2921,8 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"}, {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, @@ -2829,6 +2995,33 @@ rpy2 = [ {file = "rpy2-3.4.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a86bb6a47df7454cbf72a4d98ef1a2a8eb65efbe1082265ba60978d8d421239a"}, {file = "rpy2-3.4.5.tar.gz", hash = "sha256:5d31a5ea43f5a59f6dec30faca87edb01fc9b8affa0beae96a99be923bd7dab3"}, ] +scikit-learn = [ + {file = "scikit-learn-1.0.1.tar.gz", hash = "sha256:ac2ca9dbb754d61cfe1c83ba8483498ef951d29b93ec09d6f002847f210a99da"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:116e05fd990d9b363fc29bd3699ec2117d7da9088f6ca9a90173b240c5a063f1"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd78a2442c948536f677e2744917c37cff014559648102038822c23863741c27"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32d941f12fd7e245f01da2b82943c5ce6f1133fa5375eb80caa51457532b3e7e"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb7214103f6c36c1371dd8c166897e3528264a28f2e2e42573ba8c61ed4d7142"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46248cc6a8b72490f723c73ff2e65e62633d14cafe9d2df3a7b3f87d332a6f7e"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fecb5102f0a36c16c1361ec519a7bb0260776ef40e17393a81f530569c916a7b"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-win32.whl", hash = "sha256:02aee3b257617da0ec98dee9572b10523dc00c25b68c195ddf100c1a93b1854b"}, + {file = "scikit_learn-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:538f3a85c4980c7572f3e754f0ba8489363976ef3e7f6a94e8f1af5ae45f6f6a"}, + {file = "scikit_learn-1.0.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:59b1d6df8724003fa16b7365a3b43449ee152aa6e488dd7a19f933640bb2d7fb"}, + {file = "scikit_learn-1.0.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:515b227f01f569145dc9f86e56f4cea9f00a613fc4d074bbfc0a92ca00bff467"}, + {file = "scikit_learn-1.0.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fc75f81571137b39f9b31766e15a0e525331637e7fe8f8000a3fbfba7da3add9"}, + {file = "scikit_learn-1.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:648f4dbfdd0a1b45bf6e2e4afe3f431774c55dee05e2d28f8394d6648296f373"}, + {file = "scikit_learn-1.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53bb7c605427ab187869d7a05cd3f524a3015a90e351c1788fc3a662e7f92b69"}, + {file = "scikit_learn-1.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a800665527c1a63f7395a0baae3c89b0d97b54d2c23769c1c9879061bb80bc19"}, + {file = "scikit_learn-1.0.1-cp38-cp38-win32.whl", hash = "sha256:ee59da47e18b703f6de17d5d51b16ce086c50969d5a83db5217f0ae9372de232"}, + {file = "scikit_learn-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:ebbe4275556d3c02707bd93ae8b96d9651acd4165126e0ae64b336afa2a6dcb1"}, + {file = "scikit_learn-1.0.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:11a57405c1c3514227d0c6a0bee561c94cd1284b41e236f7a1d76b3975f77593"}, + {file = "scikit_learn-1.0.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a51fdbc116974d9715957366df73e5ec6f0a7a2afa017864c2e5f5834e6f494d"}, + {file = "scikit_learn-1.0.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:944f47b2d881b9d24aee40d643bfdc4bd2b6dc3d25b62964411c6d8882f940a1"}, + {file = "scikit_learn-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc60e0371e521995a6af2ef3f5d911568506124c272889b318b8b6e497251231"}, + {file = "scikit_learn-1.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ce4e3ddb6e6e9dcdb3e5ac7f0575dbaf56f79ce2b2edee55192b12b52df5be"}, + {file = "scikit_learn-1.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:059c5be0c0365321ddbcac7abf0db806fad8ecb64ee6c7cbcd58313c7d61634d"}, + {file = "scikit_learn-1.0.1-cp39-cp39-win32.whl", hash = "sha256:c6b9510fd2e1642314efb7aa951a0d05d963f3523e01c30b2dadde2395ebe6b4"}, + {file = "scikit_learn-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c604a813df8e7d6dfca3ae0db0a8fd7e5dff4ea9d94081ab263c81bf0b61ab4b"}, +] scipy = [ {file = "scipy-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a15a1f3fc0abff33e792d6049161b7795909b40b97c6cc2934ed54384017ab76"}, {file = "scipy-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79570979ccdc3d165456dd62041d9556fb9733b86b4b6d818af7a0afc15f092"}, @@ -2959,6 +3152,10 @@ testpath = [ {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, {file = "testpath-0.5.0.tar.gz", hash = "sha256:1acf7a0bcd3004ae8357409fc33751e16d37ccc650921da1094a86581ad1e417"}, ] +threadpoolctl = [ + {file = "threadpoolctl-3.0.0-py3-none-any.whl", hash = "sha256:4fade5b3b48ae4b1c30f200b28f39180371104fccc642e039e0f2435ec8cc211"}, + {file = "threadpoolctl-3.0.0.tar.gz", hash = "sha256:d03115321233d0be715f0d3a5ad1d6c065fe425ddc2d671ca8e45e9fd5d7a52a"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -3056,8 +3253,8 @@ tzdata = [ {file = "tzdata-2021.1.tar.gz", hash = "sha256:e19c7351f887522a1ac739d21041e592ddde6dd1b764fdefa8f7b2b3551d3d38"}, ] tzlocal = [ - {file = "tzlocal-3.0-py3-none-any.whl", hash = "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559"}, - {file = "tzlocal-3.0.tar.gz", hash = "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12"}, + {file = "tzlocal-4.1-py3-none-any.whl", hash = "sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f"}, + {file = "tzlocal-4.1.tar.gz", hash = "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09"}, ] unidecode = [ {file = "Unidecode-1.3.2-py3-none-any.whl", hash = "sha256:215fe33c9d1c889fa823ccb66df91b02524eb8cc8c9c80f9c5b8129754d27829"}, diff --git a/pyproject.toml b/pyproject.toml index 3abd298..358244b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ patsy = "^0.5.1" coverage = "^5.5" pytest-cov = "^2.11.1" h5 = "^0.4.1" -h5py = "^3.3.0" +h5py = "3.3.0" pymer4 = "^0.7.5" pytest = "^6.2.4" jupyter-dash = "^0.4.0" @@ -33,6 +33,8 @@ sphinx-autoapi = "^1.8.4" numpydoc = "^1.1.0" toml = "^0.10.2" ipykernel = "^6.3.1" +boba = "^1.1.2" +boba-visualizer = "^1.1.1" [tool.poetry.dev-dependencies] diff --git a/tests/generated_scripts/main_only.py b/tests/generated_scripts/main_only.py index 3679997..a3ba489 100644 --- a/tests/generated_scripts/main_only.py +++ b/tests/generated_scripts/main_only.py @@ -17,6 +17,7 @@ def fit_model(): model = smf.glm(formula='Dependent_variable ~ Measure_0 + Measure_1', data=df, family=sm.families.Gaussian(sm.families.links.identity())) + res = model.fit() print(res.summary()) return model diff --git a/tests/input_json_files/main_interaction.json b/tests/input_json_files/main_interaction.json index 4c556d6..8b11e35 100644 --- a/tests/input_json_files/main_interaction.json +++ b/tests/input_json_files/main_interaction.json @@ -1,5 +1,8 @@ { "input": { + "query": { + "DV": "Weight", "IVs": ["Time"] + }, "generated family, link functions": { "GammaFamily": [ "LogLink", diff --git a/tests/input_json_files/main_only.json b/tests/input_json_files/main_only.json index ba5401a..8269544 100644 --- a/tests/input_json_files/main_only.json +++ b/tests/input_json_files/main_only.json @@ -1,5 +1,8 @@ { "input": { + "query": { + "DV": "Weight", "IVs": ["Time"] + }, "generated family, link functions": { "GammaFamily": [ "LogLink", diff --git a/tests/output_decision_json_files/decisions_main_only.json b/tests/output_decision_json_files/decisions_main_only.json index ab9fb7e..25ea886 100644 --- a/tests/output_decision_json_files/decisions_main_only.json +++ b/tests/output_decision_json_files/decisions_main_only.json @@ -1,5 +1,9 @@ { "decisions": [ + { + "options": ["Weight"], + "var": "dependent_variable" + }, { "options": [ [], @@ -104,7 +108,7 @@ "log()" ] ], - "var": "family, link pairs" + "var": "family_link_pair" } ], "graph": [] diff --git a/tests/output_template_files/template_main_only.py b/tests/output_template_files/template_main_only.py index 4969418..8b1b63c 100644 --- a/tests/output_template_files/template_main_only.py +++ b/tests/output_template_files/template_main_only.py @@ -7,7 +7,7 @@ import matplotlib.pyplot as plt # for visualizing residual plots to diagnose model fit # --- (BOBA_CONFIG) -{"decisions": [{"options": [[], ["Time"]], "var": "main_effects"}, {"options": [[]], "var": "interaction_effects"}, {"options": [[]], "var": "random_effects"}, {"options": [["Gamma", "log()"], ["Gamma", "inverse_power()"], ["Gamma", "identity()"], ["Gaussian", "logit()"], ["Gaussian", "inverse_power()"], ["Gaussian", "probit()"], ["Gaussian", "Power()"], ["Gaussian", "identity()"], ["Gaussian", "log()"], ["Gaussian", "NegativeBinomial()"], ["Gaussian", "cloglog()"], ["InverseGaussian", "log()"], ["InverseGaussian", "inverse_power()"], ["InverseGaussian", "inverse_squared()"], ["InverseGaussian", "identity()"], ["Poisson", "Power(power=.5)"], ["Poisson", "log()"], ["Poisson", "identity()"], ["Tweedie", "Power()"], ["Tweedie", "log()"]], "var": "family, link pairs"}], "graph": []} +{"decisions": [{"options": ["Weight"], "var": "dependent_variable"}, {"options": [[], ["Time"]], "var": "main_effects"}, {"options": [[]], "var": "interaction_effects"}, {"options": [[]], "var": "random_effects"}, {"options": [["Gamma", "log()"], ["Gamma", "inverse_power()"], ["Gamma", "identity()"], ["Gaussian", "logit()"], ["Gaussian", "inverse_power()"], ["Gaussian", "probit()"], ["Gaussian", "Power()"], ["Gaussian", "identity()"], ["Gaussian", "log()"], ["Gaussian", "NegativeBinomial()"], ["Gaussian", "cloglog()"], ["InverseGaussian", "log()"], ["InverseGaussian", "inverse_power()"], ["InverseGaussian", "inverse_squared()"], ["InverseGaussian", "identity()"], ["Poisson", "Power(power=.5)"], ["Poisson", "log()"], ["Poisson", "identity()"], ["Tweedie", "Power()"], ["Tweedie", "log()"]], "var": "family_link_pair"}], "graph": []} # --- (END) def fit_model(): @@ -23,14 +23,13 @@ def fit_model(): dv_formula = "{{dependent_variable}} ~ " formula = dv_formula + ivs_formula - model = smf.glm(formula=formula, data=df, family=sm.families.{family}(sm.families.links.{link})) + family = {{family_link_pair}}[0] + link = {{family_link_pair}}[1] + eval(f"model = smf.glm(formula=formula, data=df, family=sm.families.{family}(sm.families.links.{link}))") res = model.fit() print(res.summary()) return model - family = {{family_link_pair}}[0] - link = {{family_link_pair}}[1] - # What should you look for in the plot? # If there is systematic bias in how residuals are distributed, you may want to try a new link or family function. You may also want to reconsider your conceptual and statistical models. diff --git a/tests/test_generate_code_helpers.py b/tests/test_generate_code_helpers.py index 2886880..e9249af 100644 --- a/tests/test_generate_code_helpers.py +++ b/tests/test_generate_code_helpers.py @@ -35,12 +35,14 @@ ### HELPERS to reduce redundancy across test cases model_template = """ model = smf.glm(formula={formula}, data=df, family=sm.families.{family_name}(sm.families.links.{link_obj})) +""" + +model_fit_template = """ res = model.fit() print(res.summary()) return model """ - def absolute_path(p: str) -> str: return os.path.join(os.path.dirname(os.path.abspath(__file__)), p) @@ -613,7 +615,8 @@ def test_generate_statsmodels_model(self): link_obj = "Power(power=.5)" reference_code = model_template.format( formula=formula, family_name=family_name, link_obj=link_obj - ) + ) + model_fit_template + self.assertEqual(code, reference_code) # def test_generate_statsmodels_code(self): diff --git a/tests/test_generate_multiverse_code_helpers.py b/tests/test_generate_multiverse_code_helpers.py index 901b10e..ee5f29c 100644 --- a/tests/test_generate_multiverse_code_helpers.py +++ b/tests/test_generate_multiverse_code_helpers.py @@ -1,6 +1,6 @@ ## TODO: Check that the Family - link pairs make sense (this might be covered earlier in the pipeline) -from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_boba_config_from_decisions, generate_multiverse_decisions, generate_multiverse_decisions_to_json, generate_template_code, boba_config_start, boba_config_end +from tisane.multiverse_code_generator import construct_all_main_options, construct_all_interaction_options, construct_all_random_options, construct_all_family_link_options, generate_boba_config_from_decisions, generate_multiverse_decisions, generate_template_code, boba_config_start, boba_config_end import os import unittest diff --git a/tests/test_main.py b/tests/test_main.py index 23c570c..980c112 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -39,8 +39,6 @@ def testMultiverseGeneration(self): ## Specify and execute query design = ts.Design(dv=weight, ivs=[week]).assign_data(df) - # ts.infer_statistical_model_from_design(design=design) - ts.infer_all_models(design=design) diff --git a/tisane/code_generator.py b/tisane/code_generator.py index 31b9ce7..f8e1cea 100644 --- a/tisane/code_generator.py +++ b/tisane/code_generator.py @@ -73,12 +73,18 @@ def show_model_diagnostics(model): pymer4_model_template = """ model = Lmer(formula={formula}, family=\"{family_name}\", data=df) +""" + +pymer4_model_fit_template = """ print(model.fit()) return model """ statsmodels_model_template = """ model = smf.glm(formula={formula}, data=df, family=sm.families.{family_name}(sm.families.links.{link_obj})) +""" + +statsmodels_model_fit_template = """ res = model.fit() print(res.summary()) return model @@ -111,6 +117,7 @@ def show_model_diagnostics(model): "load_data_from_dataframe_template": load_data_from_dataframe_template, "load_data_no_data_source": load_data_no_data_source, "model_template": pymer4_model_template, + "model_fit_template": pymer4_model_fit_template, "model_diagnostics_function_wrapper": model_diagnostics_function_wrapper, "model_diagnostics": pymer4_model_diagnostics, "main_function": main_function, @@ -123,6 +130,7 @@ def show_model_diagnostics(model): "load_data_from_dataframe_template": load_data_from_dataframe_template, "load_data_no_data_source": load_data_no_data_source, "model_template": statsmodels_model_template, + "model_fit_template": statsmodels_model_fit_template, "model_diagnostics_function_wrapper": model_diagnostics_function_wrapper, "model_diagnostics": statsmodels_model_diagnostics, "main_function": main_function, @@ -266,15 +274,19 @@ def generate_pymer4_model(statistical_model: StatisticalModel, boba_template: bo if boba_template: formula_code = "{{FORMULA}}" family_code = "{{FAMILY}}" - model_code = pymer4_code_templates["model_template"].format( - formula=formula_code, family_name=family_code + model_code = ( + pymer4_code_templates["model_template"].format( + formula=formula_code, family_name=family_code) + + pymer4_code_templates["model_fit_template"] ) else: formula_code = generate_pymer4_formula(statistical_model=statistical_model) family_code = generate_pymer4_family(statistical_model=statistical_model) # family_code = "\"" + generate_pymer4_family(statistical_model=statistical_model) + "\"" - model_code = pymer4_code_templates["model_template"].format( - formula=formula_code, family_name=family_code + model_code = ( + pymer4_code_templates["model_template"].format( + formula=formula_code, family_name=family_code) + + pymer4_code_templates["model_fit_template"] ) return model_code @@ -420,17 +432,20 @@ def generate_statsmodels_model(statistical_model: StatisticalModel, boba_templat formula_code = "{{FORMULA}}" family_code = "{{FAMILY}}" link_code = "{{LINK}}" - model_code = statsmodels_code_templates["model_template"].format( - formula=formula_code, family_name=family_code, link_obj=link_code + model_code = ( + statsmodels_code_templates["model_template"].format( + formula=formula_code, family_name=family_code, link_obj=link_code) + + statsmodels_code_templates["model_fit_template"] ) else: formula_code = generate_statsmodels_formula(statistical_model=statistical_model) family_code = generate_statsmodels_family(statistical_model=statistical_model) link_code = generate_statsmodels_link(statistical_model=statistical_model) - model_code = statsmodels_code_templates["model_template"].format( - formula=formula_code, family_name=family_code, link_obj=link_code - ) - + model_code = ( + statsmodels_code_templates["model_template"].format( + formula=formula_code, family_name=family_code, link_obj=link_code) + + statsmodels_code_templates["model_fit_template"] + ) return model_code diff --git a/tisane/main.py b/tisane/main.py index cd2c727..068730a 100644 --- a/tisane/main.py +++ b/tisane/main.py @@ -571,7 +571,6 @@ def infer_all_statistical_models_from_design(design: Design, jupyter: bool = Fal has_random_effects = True # Generate the dicitonary representing the multiverse - # decisions_file = "decisions.json" decisions = generate_multiverse_decisions(combined_dict) # Output data somewhere to read in from template.py diff --git a/tisane/multiverse_code_generator.py b/tisane/multiverse_code_generator.py index b9d183d..a1e138a 100644 --- a/tisane/multiverse_code_generator.py +++ b/tisane/multiverse_code_generator.py @@ -54,6 +54,13 @@ def write_to_path(code: str, output_path: os.PathLike): print("Writing out path") return output_path +def extract_dependent_variable(combined_dict: Dict[str, Any]) -> List[str]: + input = combined_dict["input"] + query = input["query"] + dv = query["DV"] + + return [dv] + def construct_all_main_options(combined_dict: Dict[str, Any]) -> List[str]: input = combined_dict["input"] main_effects = input["generated main effects"] @@ -99,6 +106,12 @@ def construct_all_family_link_options(combined_dict: Dict[str, Any], has_random_ def generate_multiverse_decisions(combined_dict: Dict[str, Any]) -> Dict[str, Any]: # Generate formulae decisions modularly + # Add dependent variable + dv_options = extract_dependent_variable(combined_dict=combined_dict) + dv_dict = dict() + dv_dict["var"] = "dependent_variable" + dv_dict["options"] = dv_options + # Construct main options main_options = construct_all_main_options(combined_dict=combined_dict) main_dict = dict() @@ -120,13 +133,14 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any]) -> Dict[str, An # Construct family and link pair decisions family_link_options = construct_all_family_link_options(combined_dict) family_link_dict = dict() - family_link_dict["var"] = "family, link pairs" + family_link_dict["var"] = "family_link_pair" family_link_dict["options"] = family_link_options # Combine all the decisions decisions_dict = dict() decisions_dict["graph"] = list() decisions_dict["decisions"] = list() + decisions_dict["decisions"].append(dv_dict) decisions_dict["decisions"].append(main_dict) decisions_dict["decisions"].append(interaction_dict) decisions_dict["decisions"].append(random_dict) @@ -136,13 +150,13 @@ def generate_multiverse_decisions(combined_dict: Dict[str, Any]) -> Dict[str, An return decisions_dict -def generate_multiverse_decisions_to_json(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: - decisions_dict = generate_multiverse_decisions(combined_dict=combined_dict, decisions_path=decisions_path, decisions_file=decisions_file) +# def generate_multiverse_decisions_to_json(combined_dict: Dict[str, Any], decisions_path: os.PathLike, decisions_file: os.PathLike) -> os.PathLike: +# decisions_dict = generate_multiverse_decisions(combined_dict=combined_dict, decisions_path=decisions_path, decisions_file=decisions_file) - # Write out JSON - path = write_to_json(data=decisions_dict, output_path=decisions_path, output_filename=decisions_file) +# # Write out JSON +# path = write_to_json(data=decisions_dict, output_path=decisions_path, output_filename=decisions_file) - return path +# return path # @param template_file is the output file where the code will be output def generate_template_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None], target: str = "PYTHON", has_random_effects: bool = False): @@ -225,11 +239,7 @@ def generate_boba_config_from_decisions(decisions: Dict[str, Any]) -> str: + boba_config_end ) -### Generate Boba config with decisions from decisions_path file - # [x] 1. Update so that templates contain boba config - # 2. Run boba with template ... - # boba_config = generate_boba_config_from_decisions(decisions=decisions) - +# @returns template code with Boba cnofig, modeling, and plotting residuals def generate_template_statsmodels_code(template_path: os.PathLike, decisions: Dict[str, Any], data_path: Union[os.PathLike, None]): global statsmodels_code_templates @@ -277,16 +287,31 @@ def generate_template_statsmodels_code(template_path: os.PathLike, decisions: Di + main_function ) +# Add "eval" wrapper to execute model as a function while using Boba multiverse +def wrap_model_template(model_template: str) -> str: + wrapped_code = ( + " " # for within-method spacing + + "eval(f\"" + + model_template.strip() + + "\")") + + return wrapped_code def generate_template_pymer4_model(): global pymer4_code_templates, formula_generation_code, family_link_specification_code formula_code = "formula" - family_code = "{family}" + family_code = "{family}" - model_code = formula_generation_code + pymer4_code_templates["model_template"].format( - formula=formula_code, family_name=family_code - ) + family_link_specification_code + model_code = ( + formula_generation_code + + family_link_specification_code + + "\n" # for nice formatting + + wrap_model_template(pymer4_code_templates["model_template"].format( + formula=formula_code, family_name=family_code)) + + pymer4_code_templates["model_fit_template"] + ) + return model_code @@ -297,7 +322,12 @@ def generate_template_statsmodels_model(): family_code = "{family}" link_code = "{link}" - model_code = formula_generation_code + statsmodels_code_templates["model_template"].format( - formula=formula_code, family_name=family_code, link_obj=link_code - ) + family_link_specification_code + model_code = ( + formula_generation_code + + family_link_specification_code + + wrap_model_template(statsmodels_code_templates["model_template"].format( + formula=formula_code, family_name=family_code, link_obj=link_code)) + + statsmodels_code_templates["model_fit_template"] + ) + return model_code \ No newline at end of file