model-checker 0.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
__init__.py ADDED
File without changes
@@ -0,0 +1,78 @@
1
+ # __init__.py
2
+ from importlib.metadata import version
3
+ try:
4
+ __version__ = version("model-checker")
5
+ except ImportError:
6
+ __version__ = "unknown"
7
+
8
+ # Import specific items from utils
9
+ from .utils import (
10
+ ForAll,
11
+ Exists,
12
+ bitvec_to_substates,
13
+ )
14
+
15
+ # Import model as a whole
16
+ from . import model
17
+
18
+ # Import syntactic as a whole
19
+ from . import syntactic
20
+
21
+ # Import specific items from utils
22
+ from .builder import (
23
+ make_model_for,
24
+ find_max_N,
25
+ translate,
26
+ # run_comparison, # update
27
+ # save_comparisons, # update
28
+ )
29
+
30
+ # Import specific items from semantic
31
+ from .semantic import (
32
+ Semantics,
33
+ ImpositionSemantics,
34
+ Proposition,
35
+ )
36
+
37
+ # Import specific items from primitive
38
+ from .primitive import (
39
+ AndOperator, NegationOperator, OrOperator, # extensional
40
+ TopOperator, BotOperator, # top and bottom
41
+ IdentityOperator, GroundOperator, EssenceOperator, # constitutive
42
+ RelevanceOperator, # relevance
43
+ NecessityOperator, PossibilityOperator, # modal
44
+ CounterfactualOperator, ImpositionOperator, # counterfactual
45
+ )
46
+
47
+ from .defined import (
48
+ ConditionalOperator, BiconditionalOperator, # extensional
49
+ DefEssenceOperator, DefGroundOperator, # constitutive
50
+ MightCounterfactualOperator, MightImpositionOperator, # counterfactual
51
+
52
+ )
53
+
54
+
55
+ # Import specific items from __main__
56
+ from .__main__ import (
57
+ BuildModule,
58
+ BuildExample,
59
+ main,
60
+ )
61
+
62
+ # Define the public API of the package
63
+ __all__ = [
64
+ "model", "syntactic",
65
+ "ForAll", "Exists", "bitvec_to_substates",
66
+ "make_model_for", "find_max_N", "translate", #"run_comparison", "save_comparisons",
67
+ "Semantics", "ImpositionSemantics", "Proposition",
68
+ "ConditionalOperator", "BiconditionalOperator",
69
+ "DefEssenceOperator", "DefGroundOperator",
70
+ "MightCounterfactualOperator", "MightImpositionOperator",
71
+ "AndOperator", "NegationOperator", "OrOperator",
72
+ "TopOperator", "BotOperator",
73
+ "IdentityOperator", "GroundOperator", "EssenceOperator",
74
+ "RelevanceOperator",
75
+ "NecessityOperator", "PossibilityOperator",
76
+ "CounterfactualOperator", "ImpositionOperator",
77
+ "BuildModule", "BuildExample", "main",
78
+ ]
@@ -0,0 +1,547 @@
1
+ '''
2
+ file specifies premises, conclusions, and settings.
3
+ running the file finds a model and prints the result.
4
+ to test from the Code/ directory, run: python -m src.model_checker
5
+ '''
6
+
7
+ import sys
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import argparse
12
+ import importlib.util
13
+ # Try local imports first (for development)
14
+ try:
15
+ from src.model_checker import __version__
16
+ from src.model_checker.builder import (
17
+ make_model_for,
18
+ translate,
19
+ )
20
+ from src.model_checker.model import (
21
+ SemanticDefaults,
22
+ PropositionDefaults
23
+ )
24
+ from src.model_checker.syntactic import OperatorCollection
25
+ except ImportError:
26
+ # Fall back to installed package imports
27
+ from model_checker import __version__
28
+ from model_checker.builder import (
29
+ make_model_for,
30
+ translate,
31
+ )
32
+ from model_checker.model import (
33
+ SemanticDefaults,
34
+ PropositionDefaults
35
+ )
36
+ from model_checker.syntactic import OperatorCollection
37
+
38
+
39
+ class BuildModule:
40
+ """Load and store module values with settings and validation.
41
+
42
+ This class handles loading a Python module and extracting its model checking
43
+ configuration including premises, conclusions, and various settings.
44
+ """
45
+
46
+ DEFAULT_GENERAL_SETTINGS = {
47
+ "print_impossible": False,
48
+ "print_constraints": False,
49
+ "save_output": False,
50
+ # "optimize": False,
51
+ }
52
+
53
+ def __init__(self, module_flags):
54
+ """Initialize BuildModule with module name and path.
55
+
56
+ Args:
57
+ module_name: Name of the module to load
58
+ module_path: File path to the module
59
+
60
+ Raises:
61
+ ImportError: If module cannot be loaded
62
+ """
63
+
64
+ self.module_flags = module_flags
65
+ self.module_path = module_flags.file_path
66
+ self.module_name = os.path.splitext(os.path.basename(self.module_path))[0]
67
+ self.module = self._load_module()
68
+
69
+ # Load core attributes
70
+ self.semantic_theories = self._load_attribute("semantic_theories")
71
+ self.example_range = self._load_attribute("example_range")
72
+
73
+ # Load general settings
74
+ self.general_settings = self._load_general_settings()
75
+
76
+ def _load_module(self):
77
+ """Load the Python module from file.
78
+
79
+ Returns:
80
+ The loaded module object
81
+
82
+ Raises:
83
+ ImportError: If module cannot be loaded
84
+ """
85
+ try:
86
+ spec = importlib.util.spec_from_file_location(self.module_name, self.module_path)
87
+ if spec is None or spec.loader is None:
88
+ raise ImportError("Module spec could not be loaded.")
89
+ module = importlib.util.module_from_spec(spec)
90
+ spec.loader.exec_module(module)
91
+ return module
92
+ except Exception as e:
93
+ raise ImportError(f"Failed to load the module '{self.module_name}': {e}") from e
94
+
95
+ def _load_attribute(self, attr_name):
96
+ """Checks if an attribute exists in the module and store it.
97
+
98
+ Args:
99
+ attr_name: Name of the attribute to check and store
100
+
101
+ Raises:
102
+ AttributeError: If the attribute is missing from the module
103
+ """
104
+ if not hasattr(self.module, attr_name):
105
+ raise AttributeError(
106
+ f"Module '{self.module_name}' is missing the attribute '{attr_name}'. "
107
+ )
108
+ return getattr(self.module, attr_name, {})
109
+
110
+ def _load_general_settings(self):
111
+ """Initialize all settings from module with defaults."""
112
+ general_settings = getattr(self.module, "general_settings", {})
113
+
114
+ # Initialize each setting from module_settings with fallback to defaults
115
+ for key, default_value in self.DEFAULT_GENERAL_SETTINGS.items():
116
+ if key not in general_settings:
117
+ print(f"Warning: Module {self.module_name} settings missing '{key}', using default value: {default_value}")
118
+ setattr(self, key, general_settings.get(key, default_value))
119
+
120
+ # Store complete settings dict
121
+ general_settings = {
122
+ key: getattr(self, key)
123
+ for key in self.DEFAULT_GENERAL_SETTINGS
124
+ }
125
+
126
+ return general_settings
127
+
128
+
129
+ class BuildExample:
130
+ """Class to create and store model structure with settings as attributes."""
131
+
132
+ DEFAULT_EXAMPLE = {
133
+ "premises": [],
134
+ "conclusions": [],
135
+ }
136
+
137
+ DEFAULT_EXAMPLE_SETTINGS = {
138
+ "N": 3,
139
+ "contingent": False,
140
+ "non_empty": False,
141
+ "non_null": False,
142
+ "disjoint": False,
143
+ "max_time": 1,
144
+ }
145
+
146
+ def __init__(self, module, semantic_theory, example_case):
147
+
148
+ """Initialize model structure from module flags."""
149
+ self.module = module
150
+ self.semantics, self.proposition, self.operators, self.dictionary = self._validate_semantic_theory(semantic_theory)
151
+ self.premises, self.conclusions, example_settings = self._validate_example(example_case)
152
+ self.settings = self._validate_settings(example_settings)
153
+
154
+ # # TODO: warn if no premises or no conclusions
155
+ # # Check for missing example defaults
156
+ # for key, default_val in self.DEFAULT_EXAMPLE.items():
157
+ # module_val = getattr(self.module, key, None)
158
+ # if module_val is None:
159
+ # print(f"Warning: Module {self.module_name} is missing '{key}', using default value: {default_val}")
160
+
161
+ # # TODO: warn if missing setting
162
+ # # Check for missing settings defaults
163
+ # module_settings = getattr(self.module, "settings", {})
164
+ # for key, default_val in self.DEFAULT_GENERAL_SETTINGS.items():
165
+ # if key not in module_settings:
166
+ # print(f"Warning: Module {self.module_name} settings missing '{key}', using default value: {default_val}")
167
+
168
+ # # Set settings as instance attributes
169
+ # for key, value in self.settings.items():
170
+ # setattr(self, key, value)
171
+
172
+ # Create model structure
173
+ self.model_structure = make_model_for(
174
+ self.settings,
175
+ self.premises,
176
+ self.conclusions,
177
+ self.semantics,
178
+ self.proposition,
179
+ self.operators,
180
+ )
181
+
182
+ def _validate_semantic_theory(self, semantic_theory):
183
+ """Validates that semantic_theory contains the required components in the correct order.
184
+
185
+ Args:
186
+ semantic_theory: A tuple/list containing (Semantics, Proposition, OperatorCollection)
187
+
188
+ Returns:
189
+ The validated semantic_theory
190
+
191
+ Raises:
192
+ TypeError: If any component has an invalid type
193
+ """
194
+
195
+ if not isinstance(semantic_theory, dict) or len(semantic_theory) < 3:
196
+ raise TypeError(
197
+ "semantic_theory must be a tuple/list containing exactly 3 elements: "
198
+ "(Semantics, Proposition, OperatorCollection)"
199
+ )
200
+
201
+ semantics = semantic_theory["semantics"]
202
+ proposition = semantic_theory["proposition"]
203
+ operators = semantic_theory["operators"]
204
+ dictionary = semantic_theory.get("operators", None)
205
+
206
+ # Validate semantics is subclass of SemanticDefaults
207
+ if not issubclass(semantic_theory["semantics"], SemanticDefaults):
208
+ raise TypeError(
209
+ f"First element must be a subclass of SemanticDefaults, got {type(semantics)}"
210
+ )
211
+
212
+ # Validate proposition is subclass of PropositionDefaults
213
+ if not issubclass(proposition, PropositionDefaults):
214
+ raise TypeError(
215
+ f"Second element must be a subclass of PropositionDefaults, got {type(proposition)}"
216
+ )
217
+
218
+ # Validate operators is instance of OperatorCollection
219
+ if not isinstance(operators, OperatorCollection):
220
+ raise TypeError(
221
+ f"Third element must be an instance of OperatorCollection, got {type(operators)}"
222
+ )
223
+
224
+ return semantics, proposition, operators, dictionary
225
+
226
+ def _validate_example(self, example_case):
227
+ """Validates that example_case contains lists of strings and a dictionary.
228
+
229
+ Args:
230
+ example_case: A tuple/list containing (premises, conclusions, settings)
231
+
232
+ Returns:
233
+ The validated example_case
234
+
235
+ Raises:
236
+ TypeError: If any component has an invalid type
237
+ """
238
+ if not isinstance(example_case, (tuple, list)) or len(example_case) != 3:
239
+ raise TypeError(
240
+ "example_case must be a tuple/list containing exactly 3 elements: "
241
+ "(premises, conclusions, settings)"
242
+ )
243
+
244
+ premises, conclusions, example_settings = example_case
245
+
246
+ # Validate premises is a list of strings
247
+ if not isinstance(premises, list) or not all(isinstance(p, str) for p in premises):
248
+ raise TypeError(
249
+ f"First element must be a list of strings, got {type(premises)} "
250
+ f"containing types {[type(p) for p in premises]}"
251
+ )
252
+
253
+ # Validate conclusions is a list of strings
254
+ if not isinstance(conclusions, list) or not all(isinstance(c, str) for c in conclusions):
255
+ raise TypeError(
256
+ f"Second element must be a list of strings, got {type(conclusions)} "
257
+ f"containing types {[type(c) for c in conclusions]}"
258
+ )
259
+
260
+ # Validate settings is a dictionary
261
+ if not isinstance(example_settings, dict):
262
+ raise TypeError(
263
+ f"Third element must be a dictionary, got {type(example_settings)}"
264
+ )
265
+
266
+ return example_case
267
+
268
+ def _validate_settings(self, example_settings):
269
+ """Validates and merges example settings with general settings and module flags.
270
+
271
+ Args:
272
+ example_settings: Dictionary containing settings specific to this example
273
+
274
+ Returns:
275
+ Dictionary containing the merged and validated settings with module flag overrides
276
+
277
+ The function performs three steps:
278
+ 1. Merges example settings with general settings, warning about overlaps
279
+ 2. Updates boolean settings based on module flags
280
+ 3. Returns the final merged and validated settings dictionary
281
+ """
282
+ def merge_settings(example_settings, general_settings):
283
+ """Adds example_settings to general_settings, mentioning overlaps."""
284
+ # Track and merge settings
285
+
286
+ for key, default_value in example_settings.items():
287
+ # Get value from module settings or use default
288
+ new_value = general_settings.get(key, default_value)
289
+
290
+ # Check if we're replacing an existing general setting
291
+ if new_value != default_value:
292
+ print(f"Warning: Example setting '{key}' is replacing general setting "
293
+ f"'{default_value}' with '{new_value}'")
294
+
295
+ # Update the settings
296
+ general_settings[key] = new_value
297
+
298
+ return general_settings
299
+
300
+ def disjoin_flags(all_settings):
301
+ """Update boolean settings if module flag is True."""
302
+ module_flags = self.module.module_flags
303
+ updated_settings = all_settings.copy()
304
+
305
+ for key, value in vars(module_flags).items():
306
+ if isinstance(value, bool) and value and key in updated_settings:
307
+ updated_settings[key] = value
308
+
309
+ return updated_settings
310
+
311
+ # Merge and update settings
312
+ all_settings = merge_settings(example_settings, self.module.general_settings)
313
+ updated_settings = disjoin_flags(all_settings)
314
+
315
+ return updated_settings
316
+
317
+ def print_result(self, example_name, theory_name):
318
+ """Prints resulting model or no model if none is found."""
319
+ model_structure = self.model_structure
320
+ model_structure.print_to(example_name, theory_name)
321
+ if model_structure.settings["save_output"]:
322
+ file_name, save_constraints = self.ask_save()
323
+ self.save_or_append(model_structure, file_name, save_constraints, example_name, theory_name)
324
+
325
+ def ask_save(self):
326
+ """print the model and prompt user to store the output"""
327
+ result = input("Would you like to save the output? (y/n):\n")
328
+ if not result in ['Yes', 'yes', 'Ye', 'ye', 'Y', 'y']:
329
+ return None, None
330
+ cons_input = input("\nWould you like to include the Z3 constraints? (y/n):\n")
331
+ print_cons = bool(cons_input in ['Yes', 'yes', 'Ye', 'ye', 'Y', 'y'])
332
+ file_name = input(
333
+ "\nEnter the file name in snake_case without an extension.\n"
334
+ "Leave the file name blank to append the output to the project file.\n"
335
+ "\nFile name: "
336
+ )
337
+ return file_name, print_cons
338
+
339
+ def save_or_append(self, model_structure, file_name, save_constraints, example_name, theory_name):
340
+ """Option to save or append if a model is found."""
341
+ if file_name is None:
342
+ return
343
+ if len(file_name) == 0:
344
+ with open(f"{self.module.module_path}", 'a', encoding="utf-8") as f:
345
+ print('\n"""', file=f)
346
+ model_structure.print_to(example_name, theory_name, save_constraints, f)
347
+ print('"""', file=f)
348
+ print(f"\nAppended output to {self.module.module_path}")
349
+ return
350
+
351
+ # Default or fallback path
352
+ project_name = getattr(self.module, 'module_name', 'project')
353
+ destination_dir = os.path.join(os.getcwd(), project_name)
354
+
355
+ # Ensure the directory exists
356
+ os.makedirs(destination_dir, exist_ok=True)
357
+
358
+ with open(f"{destination_dir}/{file_name}.py", 'w', encoding="utf-8") as n:
359
+ print(f'# TITLE: {file_name}.py created in {destination_dir}\n"""', file=n)
360
+ model_structure.save_to(example_name, theory_name, save_constraints, n)
361
+ print(f'\n{file_name}.py created in {destination_dir}\n')
362
+
363
+ def generate_project(name):
364
+ """
365
+ Copy the 'template/' directory to the current working directory,
366
+ rename it to the specified 'name', and rename its modules by prefixing 'name'.
367
+ """
368
+ project_name = 'project_' + name
369
+ # Dynamically resolve the source directory relative to this script
370
+ source_dir = os.path.join(os.path.dirname(__file__), 'template')
371
+ destination_dir = os.path.join(os.getcwd(), project_name)
372
+
373
+ try:
374
+ # Check if the source directory exists
375
+ if not os.path.exists(source_dir):
376
+ raise FileNotFoundError(f"The source directory '{source_dir}' was not found.")
377
+
378
+ # Check if the destination directory already exists
379
+ if os.path.exists(destination_dir):
380
+ print(f"Error: Directory '{destination_dir}' already exists.")
381
+ return
382
+
383
+ # Copy the template directory
384
+ shutil.copytree(source_dir, destination_dir)
385
+
386
+ # Rename the files in the copied directory
387
+ files_to_rename = {
388
+ "examples.py": "examples.py",
389
+ "operators.py": "operators.py",
390
+ "semantics.py": "semantics.py",
391
+ }
392
+
393
+ for old_name, new_name in files_to_rename.items():
394
+ old_path = os.path.join(destination_dir, old_name)
395
+ new_path = os.path.join(destination_dir, new_name)
396
+ if os.path.exists(old_path):
397
+ os.rename(old_path, new_path)
398
+
399
+ print(f"\nProject generated at: {destination_dir}\n")
400
+ print("The following modules were created:")
401
+ for old_name, new_name in files_to_rename.items():
402
+ print(f" {new_name}")
403
+
404
+ # Run the example script
405
+ example_script = os.path.join(destination_dir, "examples.py")
406
+ print(example_script)
407
+ if os.path.exists(example_script):
408
+ print("\nRunning the example script...")
409
+ subprocess.run(["model-checker", example_script])
410
+ else:
411
+ print(f"\nFail to run: model-checker {example_script}")
412
+
413
+ # Output the test command
414
+ print(f"\nRun the following command to test your project:\n\nmodel-checker {destination_dir}/examples.py\n")
415
+
416
+ except FileNotFoundError as e:
417
+ print(f"Error: {e}")
418
+ except Exception as e:
419
+ print(f"An unexpected error occurred: {e}")
420
+
421
+ def ask_generate_project():
422
+ """prompt user to create a test file"""
423
+ result = input("Would you like to generate a new project? (y/n): ")
424
+ if result not in {'Yes', 'yes', 'Ye', 'ye', 'Y', 'y'}:
425
+ print("You can run an example.py file that already exists with the command:\n")
426
+ print("model-checker example.py\n")
427
+ test_name = input("Enter the name of your project using snake_case: ")
428
+ generate_project(test_name)
429
+
430
+ def parse_file_and_flags():
431
+ """returns the name and path for the current script"""
432
+ # create an ArgumentParser object
433
+ parser = argparse.ArgumentParser(
434
+ prog='model-checker',
435
+ usage='%(prog)s [options] input',
436
+ description="""
437
+ Running '%(prog)s' without options or an input will prompt the user
438
+ to generate a project. To run a test on an existing file, include
439
+ the path to that file as the input.""",
440
+ epilog="""
441
+ More information can be found at:
442
+ https://github.com/benbrastmckie/ModelChecker/""",
443
+ )
444
+ parser.add_argument(
445
+ "file_path",
446
+ nargs='?',
447
+ default=None,
448
+ type=str,
449
+ help="Specifies the path to a Python.",
450
+ )
451
+ parser.add_argument(
452
+ '--contingent',
453
+ '-c',
454
+ action='store_true',
455
+ help='Overrides to make all propositions contingent.'
456
+ )
457
+ parser.add_argument(
458
+ '--non_empty',
459
+ '-e',
460
+ action='store_true',
461
+ help='Overrides to make all propositions non_empty.'
462
+ )
463
+ parser.add_argument(
464
+ '--non_null',
465
+ '-n',
466
+ action='store_true',
467
+ help='Overrides to make all propositions non_null.'
468
+ )
469
+ parser.add_argument(
470
+ '--disjoint',
471
+ '-d',
472
+ action='store_true',
473
+ help='Overrides to make all propositions have disjoint subject-matters.'
474
+ )
475
+ parser.add_argument(
476
+ '--print_constraints',
477
+ '-p',
478
+ action='store_true',
479
+ help='Overrides to print the Z3 constraints or else the unsat_core constraints if there is no model.'
480
+ )
481
+ parser.add_argument(
482
+ '--save_output',
483
+ '-s',
484
+ action='store_true',
485
+ help='Overrides to prompt user to save output.'
486
+ )
487
+ parser.add_argument(
488
+ '--print_impossible',
489
+ '-i',
490
+ action='store_true',
491
+ help='Overrides to print impossible states.'
492
+ )
493
+ parser.add_argument(
494
+ '--version',
495
+ '-v',
496
+ action='version',
497
+ version=f"%(prog)s: {__version__}",
498
+ help='Prints the version number.'
499
+ )
500
+ # parser.add_argument(
501
+ # '--optimize',
502
+ # '-o',
503
+ # action='store_true',
504
+ # help='finds the minimum value for N that is satisfiable before reaching max_time.'
505
+ # )
506
+ parser.add_argument(
507
+ '--upgrade',
508
+ '-u',
509
+ action='store_true',
510
+ help='Upgrade the package.'
511
+ )
512
+ # parse the command-line argument to get the module path
513
+ flags = parser.parse_args()
514
+ package_name = parser.prog # Get the package name from the parser
515
+ return flags, package_name
516
+
517
+ # TODO: make method for runtime and progress bar
518
+ # TODO: make method for turning on cProfile
519
+ def main():
520
+ if len(sys.argv) < 2:
521
+ ask_generate_project()
522
+ return
523
+ module_flags, package_name = parse_file_and_flags()
524
+ if module_flags.upgrade:
525
+ print("Upgrading package")
526
+ try:
527
+ subprocess.run(['pip', 'install', '--upgrade', package_name], check=True)
528
+ except subprocess.CalledProcessError as e:
529
+ print(f"Failed to upgrade {package_name}: {e}")
530
+ return
531
+
532
+ module = BuildModule(module_flags)
533
+
534
+ # TODO: check if compare = True and then:
535
+ # check if multiple semantic_theories and then:
536
+ # run comparison function
537
+
538
+ for example_name, example_case in module.example_range.items():
539
+ for theory_name, semantic_theory in module.semantic_theories.items():
540
+ dictionary = semantic_theory.get("dictionary", None)
541
+ if dictionary:
542
+ example_case = translate(example_case, dictionary)
543
+ example = BuildExample(module, semantic_theory, example_case)
544
+ example.print_result(example_name, theory_name)
545
+
546
+ if __name__ == "__main__":
547
+ main()