siliconcompiler 0.35.3__py3-none-any.whl → 0.36.0__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.
Files changed (96) hide show
  1. siliconcompiler/_metadata.py +1 -1
  2. siliconcompiler/apps/sc_issue.py +18 -2
  3. siliconcompiler/checklist.py +2 -1
  4. siliconcompiler/constraints/__init__.py +4 -1
  5. siliconcompiler/constraints/asic_component.py +49 -11
  6. siliconcompiler/constraints/asic_floorplan.py +23 -21
  7. siliconcompiler/constraints/asic_pins.py +55 -17
  8. siliconcompiler/constraints/asic_timing.py +280 -57
  9. siliconcompiler/constraints/fpga_timing.py +212 -18
  10. siliconcompiler/constraints/timing_mode.py +82 -0
  11. siliconcompiler/data/templates/replay/replay.sh.j2 +27 -14
  12. siliconcompiler/data/templates/tcl/manifest.tcl.j2 +0 -6
  13. siliconcompiler/flowgraph.py +95 -42
  14. siliconcompiler/flows/generate_openroad_rcx.py +2 -2
  15. siliconcompiler/flows/highresscreenshotflow.py +37 -0
  16. siliconcompiler/library.py +2 -1
  17. siliconcompiler/package/__init__.py +56 -51
  18. siliconcompiler/project.py +13 -2
  19. siliconcompiler/scheduler/docker.py +24 -25
  20. siliconcompiler/scheduler/scheduler.py +143 -100
  21. siliconcompiler/scheduler/schedulernode.py +138 -22
  22. siliconcompiler/scheduler/slurm.py +120 -35
  23. siliconcompiler/scheduler/taskscheduler.py +19 -23
  24. siliconcompiler/schema/_metadata.py +1 -1
  25. siliconcompiler/schema/editableschema.py +29 -0
  26. siliconcompiler/schema/namedschema.py +2 -4
  27. siliconcompiler/schema/parametervalue.py +14 -2
  28. siliconcompiler/schema_support/cmdlineschema.py +0 -3
  29. siliconcompiler/schema_support/dependencyschema.py +0 -6
  30. siliconcompiler/schema_support/option.py +82 -1
  31. siliconcompiler/schema_support/pathschema.py +7 -13
  32. siliconcompiler/schema_support/record.py +4 -3
  33. siliconcompiler/tool.py +105 -52
  34. siliconcompiler/tools/_common/tcl/sc_schema_access.tcl +0 -6
  35. siliconcompiler/tools/keplerformal/__init__.py +7 -0
  36. siliconcompiler/tools/keplerformal/lec.py +112 -0
  37. siliconcompiler/tools/klayout/__init__.py +3 -0
  38. siliconcompiler/tools/klayout/screenshot.py +66 -1
  39. siliconcompiler/tools/klayout/scripts/klayout_convert_drc_db.py +1 -0
  40. siliconcompiler/tools/klayout/scripts/klayout_export.py +11 -40
  41. siliconcompiler/tools/klayout/scripts/klayout_operations.py +1 -0
  42. siliconcompiler/tools/klayout/scripts/klayout_show.py +5 -4
  43. siliconcompiler/tools/klayout/scripts/klayout_utils.py +16 -5
  44. siliconcompiler/tools/montage/tile.py +26 -12
  45. siliconcompiler/tools/openroad/__init__.py +27 -1
  46. siliconcompiler/tools/openroad/_apr.py +107 -14
  47. siliconcompiler/tools/openroad/clock_tree_synthesis.py +1 -0
  48. siliconcompiler/tools/openroad/global_placement.py +1 -0
  49. siliconcompiler/tools/openroad/init_floorplan.py +119 -7
  50. siliconcompiler/tools/openroad/power_grid_analysis.py +174 -0
  51. siliconcompiler/tools/openroad/repair_design.py +1 -0
  52. siliconcompiler/tools/openroad/repair_timing.py +1 -0
  53. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +1 -1
  54. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +91 -18
  55. siliconcompiler/tools/openroad/scripts/apr/sc_irdrop.tcl +148 -0
  56. siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +1 -1
  57. siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +8 -10
  58. siliconcompiler/tools/openroad/scripts/common/procs.tcl +15 -6
  59. siliconcompiler/tools/openroad/scripts/common/read_liberty.tcl +2 -2
  60. siliconcompiler/tools/openroad/scripts/common/reports.tcl +7 -4
  61. siliconcompiler/tools/openroad/scripts/common/screenshot.tcl +1 -1
  62. siliconcompiler/tools/openroad/scripts/common/write_data_physical.tcl +8 -0
  63. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +16 -12
  64. siliconcompiler/tools/openroad/scripts/rcx/sc_rcx_bench.tcl +2 -4
  65. siliconcompiler/tools/openroad/scripts/sc_rdlroute.tcl +3 -1
  66. siliconcompiler/tools/openroad/write_data.py +2 -2
  67. siliconcompiler/tools/opensta/__init__.py +1 -1
  68. siliconcompiler/tools/opensta/scripts/sc_check_library.tcl +2 -2
  69. siliconcompiler/tools/opensta/scripts/sc_report_libraries.tcl +2 -2
  70. siliconcompiler/tools/opensta/scripts/sc_timing.tcl +13 -10
  71. siliconcompiler/tools/opensta/timing.py +6 -2
  72. siliconcompiler/tools/vivado/scripts/sc_bitstream.tcl +11 -0
  73. siliconcompiler/tools/vivado/scripts/sc_place.tcl +11 -0
  74. siliconcompiler/tools/vivado/scripts/sc_route.tcl +11 -0
  75. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +10 -0
  76. siliconcompiler/tools/vpr/__init__.py +28 -0
  77. siliconcompiler/tools/yosys/scripts/sc_screenshot.tcl +1 -1
  78. siliconcompiler/tools/yosys/scripts/sc_synth_asic.tcl +40 -4
  79. siliconcompiler/tools/yosys/scripts/sc_synth_fpga.tcl +15 -5
  80. siliconcompiler/tools/yosys/syn_asic.py +42 -0
  81. siliconcompiler/tools/yosys/syn_fpga.py +8 -0
  82. siliconcompiler/toolscripts/_tools.json +12 -7
  83. siliconcompiler/toolscripts/ubuntu22/install-keplerformal.sh +72 -0
  84. siliconcompiler/toolscripts/ubuntu24/install-keplerformal.sh +72 -0
  85. siliconcompiler/utils/__init__.py +243 -51
  86. siliconcompiler/utils/curation.py +89 -56
  87. siliconcompiler/utils/issue.py +6 -1
  88. siliconcompiler/utils/multiprocessing.py +46 -2
  89. siliconcompiler/utils/paths.py +21 -0
  90. siliconcompiler/utils/settings.py +162 -0
  91. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.36.0.dist-info}/METADATA +5 -4
  92. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.36.0.dist-info}/RECORD +96 -87
  93. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.36.0.dist-info}/WHEEL +0 -0
  94. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.36.0.dist-info}/entry_points.txt +0 -0
  95. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.36.0.dist-info}/licenses/LICENSE +0 -0
  96. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.36.0.dist-info}/top_level.txt +0 -0
@@ -5,15 +5,18 @@ import shutil
5
5
  import stat
6
6
  import subprocess
7
7
  import uuid
8
+ import time
8
9
 
9
10
  import os.path
10
11
 
11
- from siliconcompiler import utils
12
- from siliconcompiler.utils.curation import collect
13
- from siliconcompiler.utils.paths import collectiondir, jobdir
12
+ from typing import List, Union, Final
13
+
14
+ from siliconcompiler import utils, sc_open
15
+ from siliconcompiler.utils.paths import jobdir
14
16
  from siliconcompiler.package import RemoteResolver
15
- from siliconcompiler.flowgraph import RuntimeFlowgraph
16
17
  from siliconcompiler.scheduler import SchedulerNode
18
+ from siliconcompiler.utils.logging import SCBlankLoggerFormatter
19
+ from siliconcompiler.utils.multiprocessing import MPManager
17
20
 
18
21
 
19
22
  class SlurmSchedulerNode(SchedulerNode):
@@ -24,6 +27,10 @@ class SlurmSchedulerNode(SchedulerNode):
24
27
  It prepares a run script, a manifest, and uses the 'srun' command
25
28
  to execute the step on a compute node.
26
29
  """
30
+ __OPTIONS: Final[str] = "scheduler-slurm"
31
+
32
+ _MAX_FS_DELAY = 2
33
+ _FS_DWELL = 0.1
27
34
 
28
35
  def __init__(self, project, step, index, replay=False):
29
36
  """Initializes a SlurmSchedulerNode.
@@ -52,34 +59,31 @@ class SlurmSchedulerNode(SchedulerNode):
52
59
  """
53
60
  A static pre-processing hook for the Slurm scheduler.
54
61
 
55
- This method checks if the compilation flow starts from an entry node.
56
- If so, it calls :meth:`.collect()` to gather all necessary source files
57
- into a central location before any remote jobs are submitted. This
58
- ensures that compute nodes have access to all required source files.
62
+ This method ensures that the Slurm environment is available and loads
63
+ any existing user configuration for the scheduler.
59
64
 
60
65
  Args:
61
66
  project (Project): The project object to perform pre-processing on.
62
67
  """
63
- if os.path.exists(collectiondir(project)):
64
- # nothing to do
65
- return
66
-
67
- do_collect = False
68
- flow = project.get('option', 'flow')
69
- entry_nodes = project.get("flowgraph", flow, field="schema").get_entry_nodes()
68
+ SlurmSchedulerNode.assert_slurm()
70
69
 
71
- runtime = RuntimeFlowgraph(
72
- project.get("flowgraph", flow, field='schema'),
73
- from_steps=project.get('option', 'from'),
74
- to_steps=project.get('option', 'to'),
75
- prune_nodes=project.get('option', 'prune'))
70
+ @staticmethod
71
+ def _set_user_config(tag: str, value: Union[List[str], str]) -> None:
72
+ """
73
+ Sets a specific value in the user configuration map.
76
74
 
77
- for (step, index) in runtime.get_nodes():
78
- if (step, index) in entry_nodes:
79
- do_collect = True
75
+ Args:
76
+ tag (str): The configuration key to update.
77
+ value (Union[List[str], str]): The value to assign to the key.
78
+ """
79
+ MPManager.get_settings().set(SlurmSchedulerNode.__OPTIONS, tag, value)
80
80
 
81
- if do_collect:
82
- collect(project)
81
+ @staticmethod
82
+ def _write_user_config() -> None:
83
+ """
84
+ Writes the current system configuration to the user configuration file.
85
+ """
86
+ MPManager.get_settings().save()
83
87
 
84
88
  @property
85
89
  def is_local(self):
@@ -150,6 +154,49 @@ class SlurmSchedulerNode(SchedulerNode):
150
154
  # Return the first listed partition
151
155
  return sinfo['nodes'][0]['partitions'][0]
152
156
 
157
+ @staticmethod
158
+ def assert_slurm() -> None:
159
+ """
160
+ Check if slurm is installed and throw error when not installed.
161
+ """
162
+ if shutil.which('sinfo') is None:
163
+ raise RuntimeError('slurm is not available or installed on this machine')
164
+
165
+ def mark_copy(self) -> bool:
166
+ sharedprefix: List[str] = MPManager.get_settings().get(
167
+ SlurmSchedulerNode.__OPTIONS, "sharedpaths", default=[])
168
+
169
+ if "/" in sharedprefix:
170
+ # Entire filesystem is shared so no need to check
171
+ return False
172
+
173
+ do_collect = False
174
+ for key in self.get_required_path_keys():
175
+ mark_copy = True
176
+ if sharedprefix:
177
+ mark_copy = False
178
+
179
+ check_step, check_index = self.step, self.index
180
+ if self.project.get(*key, field='pernode').is_never():
181
+ check_step, check_index = None, None
182
+
183
+ paths = self.project.find_files(*key, missing_ok=True,
184
+ step=check_step, index=check_index)
185
+ if not isinstance(paths, list):
186
+ paths = [paths]
187
+ paths = [str(path) for path in paths if path]
188
+
189
+ for path in paths:
190
+ if not any([path.startswith(shared) for shared in sharedprefix]):
191
+ # File exists outside shared paths and needs to be copied
192
+ mark_copy = True
193
+ break
194
+
195
+ if mark_copy:
196
+ self.project.set(*key, True, field='copy')
197
+ do_collect = True
198
+ return do_collect
199
+
153
200
  def run(self):
154
201
  """
155
202
  Runs the node's task as a job on a Slurm cluster.
@@ -161,12 +208,8 @@ class SlurmSchedulerNode(SchedulerNode):
161
208
 
162
209
  self._init_run_logger()
163
210
 
164
- if shutil.which('sinfo') is None:
165
- raise RuntimeError('slurm is not available or installed on this machine')
166
-
167
211
  # Determine which cluster parititon to use.
168
- partition = self.project.get('option', 'scheduler', 'queue',
169
- step=self.step, index=self.index)
212
+ partition = self.project.option.scheduler.get_queue(step=self.step, index=self.index)
170
213
  if not partition:
171
214
  partition = SlurmSchedulerNode.get_slurm_partition()
172
215
 
@@ -191,7 +234,7 @@ class SlurmSchedulerNode(SchedulerNode):
191
234
  with open(script_file, 'w') as sf:
192
235
  sf.write(utils.get_file_template('slurm/run.sh').render(
193
236
  cfg_file=shlex.quote(cfg_file),
194
- build_dir=shlex.quote(self.project.get("option", "builddir")),
237
+ build_dir=shlex.quote(self.project.option.get_builddir()),
195
238
  step=shlex.quote(self.step),
196
239
  index=shlex.quote(self.index),
197
240
  cachedir=shlex.quote(str(RemoteResolver.determine_cache_dir(self.project)))
@@ -202,7 +245,6 @@ class SlurmSchedulerNode(SchedulerNode):
202
245
  os.stat(script_file).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
203
246
 
204
247
  schedule_cmd = ['srun',
205
- '--exclusive',
206
248
  '--partition', partition,
207
249
  '--chdir', self.project_cwd,
208
250
  '--job-name', SlurmSchedulerNode.get_job_name(self.__job_hash,
@@ -210,16 +252,22 @@ class SlurmSchedulerNode(SchedulerNode):
210
252
  '--output', log_file]
211
253
 
212
254
  # Only delay the starting time if the 'defer' Schema option is specified.
213
- defer_time = self.project.get('option', 'scheduler', 'defer',
214
- step=self.step, index=self.index)
255
+ defer_time = self.project.option.scheduler.get_defer(step=self.step, index=self.index)
215
256
  if defer_time:
216
257
  schedule_cmd.extend(['--begin', defer_time])
217
258
 
259
+ # Forward additional user options
260
+ options = self.project.option.scheduler.get_options(step=self.step, index=self.index)
261
+ if options:
262
+ schedule_cmd.extend(options)
263
+
218
264
  schedule_cmd.append(script_file)
219
265
 
266
+ self.logger.debug(f"Executing slurm command: {shlex.join(schedule_cmd)}")
267
+
220
268
  # Run the 'srun' command, and track its output.
221
- # TODO: output should be fed to log, and stdout if quiet = False
222
269
  step_result = subprocess.Popen(schedule_cmd,
270
+ stdin=subprocess.DEVNULL,
223
271
  stdout=subprocess.PIPE,
224
272
  stderr=subprocess.STDOUT)
225
273
 
@@ -227,3 +275,40 @@ class SlurmSchedulerNode(SchedulerNode):
227
275
  # as it has closed its output stream. But if we don't call '.wait()',
228
276
  # the '.returncode' value will not be set correctly.
229
277
  step_result.wait()
278
+
279
+ # Attempt to list dir to trigger network FS to update
280
+ try:
281
+ os.listdir(os.path.dirname(log_file))
282
+ except: # noqa E722
283
+ pass
284
+
285
+ # Print the log to logger
286
+ if os.path.exists(log_file):
287
+ org_formatter = self.project._logger_console.formatter
288
+ try:
289
+ self.project._logger_console.setFormatter(SCBlankLoggerFormatter())
290
+ with sc_open(log_file) as log:
291
+ for line in log.readlines():
292
+ self.logger.info(line.rstrip())
293
+ finally:
294
+ self.project._logger_console.setFormatter(org_formatter)
295
+
296
+ if step_result.returncode != 0:
297
+ self.logger.error(f"Slurm exited with a non-zero code ({step_result.returncode}).")
298
+ if os.path.exists(log_file):
299
+ self.logger.error(f"Node log file: {log_file}")
300
+ self.halt()
301
+
302
+ # Wait for manifest to propagate through network filesystem
303
+ start = time.time()
304
+ elapsed = 0
305
+ manifest_path = self.get_manifest()
306
+ while not os.path.exists(manifest_path) and elapsed <= SlurmSchedulerNode._MAX_FS_DELAY:
307
+ os.listdir(os.path.dirname(manifest_path))
308
+ elapsed = time.time() - start
309
+ time.sleep(SlurmSchedulerNode._FS_DWELL)
310
+ if not os.path.exists(manifest_path):
311
+ self.logger.error(f"Manifest was not created on time: {manifest_path}")
312
+
313
+ def check_required_paths(self) -> bool:
314
+ return True
@@ -5,7 +5,7 @@ import time
5
5
 
6
6
  import os.path
7
7
 
8
- from typing import List, Dict, Tuple, Optional, Callable, ClassVar, Any, Literal, TYPE_CHECKING
8
+ from typing import List, Dict, Tuple, Optional, Callable, Any, Literal, TYPE_CHECKING
9
9
 
10
10
  from logging.handlers import QueueListener
11
11
 
@@ -35,12 +35,6 @@ class TaskScheduler:
35
35
  dependency checking. It operates on a set of pending tasks defined by the
36
36
  main Scheduler and executes them in a loop until the flow is complete.
37
37
  """
38
- __callbacks: ClassVar[Dict[str, Callable[..., None]]] = {
39
- "pre_run": lambda project: None,
40
- "pre_node": lambda project, step, index: None,
41
- "post_node": lambda project, step, index: None,
42
- "post_run": lambda project: None,
43
- }
44
38
 
45
39
  @staticmethod
46
40
  def register_callback(hook: Literal["pre_run", "pre_node", "post_node", "post_run"],
@@ -57,9 +51,9 @@ class TaskScheduler:
57
51
  Raises:
58
52
  ValueError: If the specified hook is not valid.
59
53
  """
60
- if hook not in TaskScheduler.__callbacks:
54
+ if hook not in ('pre_run', 'pre_node', 'post_node', 'post_run'):
61
55
  raise ValueError(f"{hook} is not a valid callback")
62
- TaskScheduler.__callbacks[hook] = func
56
+ MPManager.get_transient_settings().set('TaskScheduler', hook, func)
63
57
 
64
58
  def __init__(self, project: "Project", tasks: Dict[Tuple[str, str], "SchedulerNode"]):
65
59
  """Initializes the TaskScheduler.
@@ -117,8 +111,6 @@ class TaskScheduler:
117
111
  from_steps=set([step for step, _ in self.__flow.get_entry_nodes()]),
118
112
  prune_nodes=self.__project.option.get_prune())
119
113
 
120
- init_funcs = set()
121
-
122
114
  for step, index in self.__runtime_flow.get_nodes():
123
115
  if self.__record.get('status', step=step, index=index) != NodeStatus.PENDING:
124
116
  continue
@@ -141,11 +133,7 @@ class TaskScheduler:
141
133
  threads = self.__max_threads
142
134
  task["threads"] = max(1, min(threads, self.__max_threads))
143
135
 
144
- task["parent_pipe"], pipe = multiprocessing.Pipe()
145
- task["node"].set_queue(pipe, self.__log_queue)
146
-
147
136
  task["proc"] = multiprocessing.Process(target=task["node"].run)
148
- init_funcs.add(task["node"].init)
149
137
  self.__nodes[(step, index)] = task
150
138
 
151
139
  # Create ordered list of nodes
@@ -155,10 +143,6 @@ class TaskScheduler:
155
143
  if node in self.__nodes:
156
144
  self.__ordered_nodes.append(node)
157
145
 
158
- # Call preprocessing for schedulers
159
- for init_func in init_funcs:
160
- init_func(self.__project)
161
-
162
146
  def run(self, job_log_handler: logging.Handler) -> None:
163
147
  """
164
148
  The main entry point for the task scheduling loop.
@@ -188,11 +172,13 @@ class TaskScheduler:
188
172
  if self.__dashboard:
189
173
  self.__dashboard.update_manifest()
190
174
 
191
- TaskScheduler.__callbacks["pre_run"](self.__project)
175
+ MPManager.get_transient_settings().get(
176
+ 'TaskScheduler', 'pre_run', lambda project: None)(self.__project)
192
177
 
193
178
  try:
194
179
  self.__run_loop()
195
- TaskScheduler.__callbacks["post_run"](self.__project)
180
+ MPManager.get_transient_settings().get(
181
+ 'TaskScheduler', 'post_run', lambda project: None)(self.__project)
196
182
  except KeyboardInterrupt:
197
183
  # exit immediately
198
184
  log_listener.stop()
@@ -311,6 +297,10 @@ class TaskScheduler:
311
297
  except: # noqa E722
312
298
  pass
313
299
 
300
+ # Remove pipe
301
+ info["parent_pipe"] = None
302
+ info["node"].set_queue(None, None)
303
+
314
304
  step, index = node
315
305
  if info["proc"].exitcode > 0:
316
306
  status = NodeStatus.ERROR
@@ -326,7 +316,9 @@ class TaskScheduler:
326
316
 
327
317
  changed = True
328
318
 
329
- TaskScheduler.__callbacks['post_node'](self.__project, step, index)
319
+ MPManager.get_transient_settings().get(
320
+ 'TaskScheduler', 'post_node',
321
+ lambda project, step, index: None)(self.__project, step, index)
330
322
 
331
323
  return changed
332
324
 
@@ -413,7 +405,9 @@ class TaskScheduler:
413
405
  if ready and self.__allow_start(node):
414
406
  self.__logger.debug(f'Launching {info["name"]}')
415
407
 
416
- TaskScheduler.__callbacks['pre_node'](self.__project, step, index)
408
+ MPManager.get_transient_settings().get(
409
+ 'TaskScheduler', 'pre_node',
410
+ lambda project, step, index: None)(self.__project, step, index)
417
411
 
418
412
  self.__record.set('status', NodeStatus.RUNNING, step=step, index=index)
419
413
  self.__startTimes[node] = time.time()
@@ -421,6 +415,8 @@ class TaskScheduler:
421
415
 
422
416
  # Start the process
423
417
  info["running"] = True
418
+ info["parent_pipe"], pipe = multiprocessing.Pipe()
419
+ info["node"].set_queue(pipe, self.__log_queue)
424
420
  info["proc"].start()
425
421
 
426
422
  return changed
@@ -1,2 +1,2 @@
1
1
  # Version number following semver standard.
2
- version = '0.52.1'
2
+ version = '0.53.1'
@@ -6,6 +6,7 @@
6
6
 
7
7
  from .parameter import Parameter
8
8
  from .baseschema import BaseSchema
9
+ from .namedschema import NamedSchema
9
10
 
10
11
  from typing import Union, Tuple
11
12
 
@@ -38,6 +39,11 @@ class EditableSchema:
38
39
  if isinstance(value, BaseSchema):
39
40
  value._BaseSchema__parent = self.__schema
40
41
  value._BaseSchema__key = key
42
+ if isinstance(value, NamedSchema):
43
+ if key == "default":
44
+ value._NamedSchema__name = None
45
+ else:
46
+ value._NamedSchema__name = key
41
47
 
42
48
  if key == "default":
43
49
  self.__schema._BaseSchema__default = value
@@ -146,3 +152,26 @@ class EditableSchema:
146
152
  raise ValueError("Keypath must only be strings")
147
153
 
148
154
  return self.__schema._BaseSchema__search(*keypath, require_leaf=False)
155
+
156
+ def copy(self) -> BaseSchema:
157
+ '''
158
+ Creates a copy of the schema object, disconnected from any parent schema
159
+ '''
160
+
161
+ new_schema = self.__schema.copy()
162
+ if new_schema._parent() is not new_schema:
163
+ new_schema._BaseSchema__parent = None
164
+ return new_schema
165
+
166
+ def rename(self, name: str):
167
+ '''
168
+ Renames a named schema
169
+ '''
170
+
171
+ if not isinstance(self.__schema, NamedSchema):
172
+ raise TypeError("schema must be a named schema")
173
+
174
+ if self.__schema._parent() is not self.__schema:
175
+ raise ValueError("object is already in a schema")
176
+
177
+ self.__schema._NamedSchema__name = name
@@ -44,12 +44,10 @@ class NamedSchema(BaseSchema):
44
44
  """
45
45
 
46
46
  try:
47
- if self.__name is not None:
48
- raise RuntimeError("Cannot call set_name more than once.")
47
+ if self._parent() is not self:
48
+ raise RuntimeError("Cannot call set_name after it has been inserted into schema.")
49
49
  except AttributeError:
50
50
  pass
51
- if name is not None and "." in name:
52
- raise ValueError("Named schema object cannot contains: .")
53
51
  self.__name = name
54
52
 
55
53
  def type(self) -> str:
@@ -185,7 +185,13 @@ class NodeListValue:
185
185
  '''
186
186
  Returns a copy of the values stored in the list
187
187
  '''
188
- return self.__values.copy()
188
+ if self.__values:
189
+ return self.__values.copy()
190
+ else:
191
+ if self.__base.has_value:
192
+ return [self.__base]
193
+ else:
194
+ return []
189
195
 
190
196
  def copy(self) -> "NodeListValue":
191
197
  """
@@ -385,7 +391,13 @@ class NodeSetValue:
385
391
  '''
386
392
  Returns a copy of the values stored in the list
387
393
  '''
388
- return self.__values.copy()
394
+ if self.__values:
395
+ return self.__values.copy()
396
+ else:
397
+ if self.__base.has_value:
398
+ return [self.__base]
399
+ else:
400
+ return []
389
401
 
390
402
  def copy(self) -> "NodeSetValue":
391
403
  """
@@ -172,9 +172,6 @@ class CommandLineSchema(BaseSchema):
172
172
 
173
173
  # Iterate over all keys from an empty schema to add parser arguments
174
174
  for keypath in sorted(keyschema.allkeys()):
175
- if keypath == ("option", "cfg"): # TODO: remove this when cfg is removed from schema
176
- continue
177
-
178
175
  param: Parameter = keyschema.get(*keypath, field=None)
179
176
 
180
177
  dest, switches = param.add_commandline_arguments(
@@ -210,12 +210,6 @@ class DependencySchema(BaseSchema):
210
210
 
211
211
  if name:
212
212
  if not self.has_dep(name):
213
- if "." in name:
214
- name0, *name1 = name.split(".")
215
- subdep = self.get_dep(name0)
216
- if isinstance(subdep, DependencySchema):
217
- return subdep.get_dep(".".join(name1))
218
- raise KeyError(f"{name} does not contain dependency information")
219
213
  raise KeyError(f"{name} is not an imported module")
220
214
 
221
215
  return self.__deps[name]
@@ -1,7 +1,8 @@
1
- from typing import Union, List, Tuple, Callable, Dict, Optional
1
+ from typing import Union, List, Tuple, Callable, Dict, Optional, Final
2
2
 
3
3
  from siliconcompiler.schema import BaseSchema, EditableSchema, Parameter, Scope, PerNode
4
4
  from siliconcompiler.schema.utils import trim
5
+ from siliconcompiler.utils.multiprocessing import MPManager
5
6
 
6
7
 
7
8
  class SchedulerSchema(BaseSchema):
@@ -427,6 +428,8 @@ class OptionSchema(BaseSchema):
427
428
  compiler's behavior, such as flow control, logging, build settings, and
428
429
  remote execution. It provides getter and setter methods for each parameter.
429
430
  """
431
+ __OPTIONS: Final[str] = "schema-options"
432
+
430
433
  def __init__(self):
431
434
  """Initializes the options schema and defines all its parameters."""
432
435
  super().__init__()
@@ -844,6 +847,84 @@ class OptionSchema(BaseSchema):
844
847
 
845
848
  schema.insert('scheduler', SchedulerSchema())
846
849
 
850
+ self.__load_defaults()
851
+
852
+ def __load_defaults(self) -> None:
853
+ """Loads and applies settings from the default options file.
854
+
855
+ This method reads the configuration file specified by the settings
856
+ manager. It iterates through the list of option
857
+ objects in the file.
858
+
859
+ For each object, it checks for a "key" and a "value". If the key
860
+ is recognized (exists in `self.allkeys()`), it attempts to apply
861
+ the value using `self.set()`.
862
+
863
+ Errors during value setting (`ValueError`) are silently ignored.
864
+ """
865
+ options = MPManager.get_settings().get_category(OptionSchema.__OPTIONS)
866
+
867
+ if not options:
868
+ return
869
+
870
+ allkeys = self.allkeys()
871
+ for key, value in options.items():
872
+ if key is None:
873
+ continue
874
+
875
+ key = tuple(key.split(","))
876
+ if key not in allkeys:
877
+ continue
878
+
879
+ try:
880
+ self.set(*key, value)
881
+ except ValueError:
882
+ pass
883
+
884
+ def write_defaults(self) -> None:
885
+ """Saves all non-default settings to the configuration file.
886
+
887
+ This method iterates through all parameters known to the system
888
+ (via `self.allkeys()`). It compares the current value of each
889
+ parameter against its default value.
890
+
891
+ Any parameter whose current value differs from its default is
892
+ collected. This list of non-default settings is then
893
+ serialized as a JSON array to the file specified by
894
+ `default_options_file()`.
895
+
896
+ If all parameters are set to their default values, the list
897
+ will be empty, and no file will be written.
898
+ """
899
+ transientkeys = {
900
+ # Flow information
901
+ ("flow",),
902
+ ("from",),
903
+ ("to",),
904
+ ("prune",),
905
+
906
+ # Design information
907
+ ("design",),
908
+ ("alias",),
909
+ ("fileset",),
910
+ }
911
+
912
+ settings = MPManager.get_settings()
913
+ settings.delete(OptionSchema.__OPTIONS)
914
+
915
+ for key in self.allkeys():
916
+ if key in transientkeys:
917
+ continue
918
+
919
+ param: Parameter = self.get(*key, field=None)
920
+
921
+ value = param.get()
922
+ if value != param.default.get():
923
+ settings.set(OptionSchema.__OPTIONS, ",".join(key), value)
924
+
925
+ if settings.get_category(OptionSchema.__OPTIONS):
926
+ settings.save()
927
+
847
928
  # Getters and Setters
848
929
  def get_remote(self) -> bool:
849
930
  """Gets the remote processing flag.
@@ -11,7 +11,7 @@ from siliconcompiler.schema.parameter import Parameter, Scope
11
11
  from siliconcompiler.schema.utils import trim
12
12
 
13
13
  from siliconcompiler.package import Resolver
14
- from siliconcompiler.utils.paths import collectiondir
14
+ from siliconcompiler.utils.paths import collectiondir, cwdirsafe
15
15
 
16
16
 
17
17
  class PathSchemaBase(BaseSchema):
@@ -51,14 +51,12 @@ class PathSchemaBase(BaseSchema):
51
51
  the schema.
52
52
  """
53
53
  schema_root = self._parent(root=True)
54
- cwd = getattr(schema_root, "_Project__cwd", os.getcwd())
55
- collection_dir = collectiondir(schema_root)
56
54
 
57
55
  return super()._find_files(*keypath,
58
56
  missing_ok=missing_ok,
59
57
  step=step, index=index,
60
- collection_dir=collection_dir,
61
- cwd=cwd)
58
+ collection_dir=collectiondir(schema_root),
59
+ cwd=cwdirsafe(schema_root))
62
60
 
63
61
  def check_filepaths(self, ignore_keys: Optional[List[Tuple[str, ...]]] = None) -> bool:
64
62
  '''
@@ -71,17 +69,15 @@ class PathSchemaBase(BaseSchema):
71
69
  True if all file paths are valid, otherwise False.
72
70
  '''
73
71
  schema_root = self._parent(root=True)
74
- cwd = getattr(schema_root, "_Project__cwd", os.getcwd())
75
72
  logger = getattr(schema_root,
76
73
  "logger",
77
74
  logging.getLogger("siliconcompiler.check_filepaths"))
78
- collection_dir = collectiondir(schema_root)
79
75
 
80
76
  return super()._check_filepaths(
81
77
  ignore_keys=ignore_keys,
82
78
  logger=logger,
83
- collection_dir=collection_dir,
84
- cwd=cwd)
79
+ collection_dir=collectiondir(schema_root),
80
+ cwd=cwdirsafe(schema_root))
85
81
 
86
82
  def hash_files(self, *keypath: str,
87
83
  update: bool = True,
@@ -126,11 +122,9 @@ class PathSchemaBase(BaseSchema):
126
122
  Computes, stores, and returns hashes of files in :keypath:`input, rtl, verilog`.
127
123
  '''
128
124
  schema_root = self._parent(root=True)
129
- cwd = getattr(schema_root, "_Project__cwd", os.getcwd())
130
125
  logger = getattr(schema_root,
131
126
  "logger",
132
127
  logging.getLogger("siliconcompiler.hash_files"))
133
- collection_dir = collectiondir(schema_root)
134
128
 
135
129
  if verbose:
136
130
  logger.info(f"Computing hash value for [{','.join([*self._keypath, *keypath])}]")
@@ -138,8 +132,8 @@ class PathSchemaBase(BaseSchema):
138
132
  hashes = super()._hash_files(*keypath,
139
133
  missing_ok=missing_ok,
140
134
  step=step, index=index,
141
- collection_dir=collection_dir,
142
- cwd=cwd)
135
+ collection_dir=collectiondir(schema_root),
136
+ cwd=cwdirsafe(schema_root))
143
137
 
144
138
  if check:
145
139
  check_hashes = self.get(*keypath, field="filehash", step=step, index=index)
@@ -15,6 +15,7 @@ from enum import Enum
15
15
  from siliconcompiler.schema import BaseSchema, LazyLoad
16
16
  from siliconcompiler.schema import EditableSchema, Parameter, PerNode, Scope
17
17
  from siliconcompiler.schema.utils import trim
18
+ from siliconcompiler.utils.multiprocessing import MPManager
18
19
 
19
20
  from siliconcompiler import _metadata
20
21
 
@@ -138,9 +139,9 @@ class RecordSchema(BaseSchema):
138
139
  "region": str
139
140
  }
140
141
  '''
141
- # TODO: add logic to figure out if we're running on a remote cluster and
142
- # extract the region in a provider-specific way.
143
- return {"region": "local"}
142
+
143
+ region = MPManager().get_settings().get("record", "region", default="local")
144
+ return {"region": region}
144
145
 
145
146
  @staticmethod
146
147
  def get_ip_information() -> Dict[str, Optional[str]]: