siliconcompiler 0.35.3__py3-none-any.whl → 0.35.4__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 (68) 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/asic_component.py +49 -11
  5. siliconcompiler/constraints/asic_floorplan.py +23 -21
  6. siliconcompiler/constraints/asic_pins.py +55 -17
  7. siliconcompiler/constraints/asic_timing.py +53 -22
  8. siliconcompiler/constraints/fpga_timing.py +5 -6
  9. siliconcompiler/data/templates/replay/replay.sh.j2 +27 -14
  10. siliconcompiler/package/__init__.py +17 -6
  11. siliconcompiler/project.py +9 -1
  12. siliconcompiler/scheduler/docker.py +24 -25
  13. siliconcompiler/scheduler/scheduler.py +82 -68
  14. siliconcompiler/scheduler/schedulernode.py +133 -20
  15. siliconcompiler/scheduler/slurm.py +113 -29
  16. siliconcompiler/scheduler/taskscheduler.py +0 -7
  17. siliconcompiler/schema/editableschema.py +29 -0
  18. siliconcompiler/schema/parametervalue.py +14 -2
  19. siliconcompiler/schema_support/option.py +82 -1
  20. siliconcompiler/schema_support/pathschema.py +7 -13
  21. siliconcompiler/tool.py +47 -25
  22. siliconcompiler/tools/klayout/__init__.py +3 -0
  23. siliconcompiler/tools/klayout/scripts/klayout_convert_drc_db.py +1 -0
  24. siliconcompiler/tools/klayout/scripts/klayout_export.py +1 -0
  25. siliconcompiler/tools/klayout/scripts/klayout_operations.py +1 -0
  26. siliconcompiler/tools/klayout/scripts/klayout_show.py +1 -0
  27. siliconcompiler/tools/klayout/scripts/klayout_utils.py +3 -4
  28. siliconcompiler/tools/openroad/__init__.py +27 -1
  29. siliconcompiler/tools/openroad/_apr.py +81 -4
  30. siliconcompiler/tools/openroad/clock_tree_synthesis.py +1 -0
  31. siliconcompiler/tools/openroad/global_placement.py +1 -0
  32. siliconcompiler/tools/openroad/init_floorplan.py +116 -7
  33. siliconcompiler/tools/openroad/power_grid_analysis.py +174 -0
  34. siliconcompiler/tools/openroad/repair_design.py +1 -0
  35. siliconcompiler/tools/openroad/repair_timing.py +1 -0
  36. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +1 -1
  37. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +42 -4
  38. siliconcompiler/tools/openroad/scripts/apr/sc_irdrop.tcl +146 -0
  39. siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +1 -1
  40. siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +4 -6
  41. siliconcompiler/tools/openroad/scripts/common/procs.tcl +1 -1
  42. siliconcompiler/tools/openroad/scripts/common/reports.tcl +1 -1
  43. siliconcompiler/tools/openroad/scripts/rcx/sc_rcx_bench.tcl +2 -4
  44. siliconcompiler/tools/opensta/__init__.py +1 -1
  45. siliconcompiler/tools/opensta/scripts/sc_timing.tcl +17 -12
  46. siliconcompiler/tools/vivado/scripts/sc_bitstream.tcl +11 -0
  47. siliconcompiler/tools/vivado/scripts/sc_place.tcl +11 -0
  48. siliconcompiler/tools/vivado/scripts/sc_route.tcl +11 -0
  49. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +10 -0
  50. siliconcompiler/tools/vpr/__init__.py +28 -0
  51. siliconcompiler/tools/yosys/scripts/sc_screenshot.tcl +1 -1
  52. siliconcompiler/tools/yosys/scripts/sc_synth_asic.tcl +40 -4
  53. siliconcompiler/tools/yosys/scripts/sc_synth_fpga.tcl +15 -5
  54. siliconcompiler/tools/yosys/syn_asic.py +42 -0
  55. siliconcompiler/tools/yosys/syn_fpga.py +8 -0
  56. siliconcompiler/toolscripts/_tools.json +6 -6
  57. siliconcompiler/utils/__init__.py +243 -51
  58. siliconcompiler/utils/curation.py +89 -56
  59. siliconcompiler/utils/issue.py +6 -1
  60. siliconcompiler/utils/multiprocessing.py +35 -2
  61. siliconcompiler/utils/paths.py +21 -0
  62. siliconcompiler/utils/settings.py +141 -0
  63. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/METADATA +4 -3
  64. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/RECORD +68 -65
  65. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/WHEEL +0 -0
  66. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/entry_points.txt +0 -0
  67. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/licenses/LICENSE +0 -0
  68. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.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,9 +208,6 @@ 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
212
  partition = self.project.get('option', 'scheduler', 'queue',
169
213
  step=self.step, index=self.index)
@@ -191,7 +235,7 @@ class SlurmSchedulerNode(SchedulerNode):
191
235
  with open(script_file, 'w') as sf:
192
236
  sf.write(utils.get_file_template('slurm/run.sh').render(
193
237
  cfg_file=shlex.quote(cfg_file),
194
- build_dir=shlex.quote(self.project.get("option", "builddir")),
238
+ build_dir=shlex.quote(self.project.option.get_builddir()),
195
239
  step=shlex.quote(self.step),
196
240
  index=shlex.quote(self.index),
197
241
  cachedir=shlex.quote(str(RemoteResolver.determine_cache_dir(self.project)))
@@ -217,9 +261,12 @@ class SlurmSchedulerNode(SchedulerNode):
217
261
 
218
262
  schedule_cmd.append(script_file)
219
263
 
264
+ self.logger.debug(f"Executing slurm command: {shlex.join(schedule_cmd)}")
265
+
220
266
  # Run the 'srun' command, and track its output.
221
267
  # TODO: output should be fed to log, and stdout if quiet = False
222
268
  step_result = subprocess.Popen(schedule_cmd,
269
+ stdin=subprocess.DEVNULL,
223
270
  stdout=subprocess.PIPE,
224
271
  stderr=subprocess.STDOUT)
225
272
 
@@ -227,3 +274,40 @@ class SlurmSchedulerNode(SchedulerNode):
227
274
  # as it has closed its output stream. But if we don't call '.wait()',
228
275
  # the '.returncode' value will not be set correctly.
229
276
  step_result.wait()
277
+
278
+ # Attempt to list dir to trigger network FS to update
279
+ try:
280
+ os.listdir(os.path.dirname(log_file))
281
+ except: # noqa E722
282
+ pass
283
+
284
+ # Print the log to logger
285
+ if os.path.exists(log_file):
286
+ org_formatter = self.project._logger_console.formatter
287
+ try:
288
+ self.project._logger_console.setFormatter(SCBlankLoggerFormatter())
289
+ with sc_open(log_file) as log:
290
+ for line in log.readlines():
291
+ self.logger.info(line.rstrip())
292
+ finally:
293
+ self.project._logger_console.setFormatter(org_formatter)
294
+
295
+ if step_result.returncode != 0:
296
+ self.logger.error(f"Slurm exited with a non-zero code ({step_result.returncode}).")
297
+ if os.path.exists(log_file):
298
+ self.logger.error(f"Node log file: {log_file}")
299
+ self.halt()
300
+
301
+ # Wait for manifest to propagate through network filesystem
302
+ start = time.time()
303
+ elapsed = 0
304
+ manifest_path = self.get_manifest()
305
+ while not os.path.exists(manifest_path) and elapsed <= SlurmSchedulerNode._MAX_FS_DELAY:
306
+ os.listdir(os.path.dirname(manifest_path))
307
+ elapsed = time.time() - start
308
+ time.sleep(SlurmSchedulerNode._FS_DWELL)
309
+ if not os.path.exists(manifest_path):
310
+ self.logger.error(f"Manifest was not created on time: {manifest_path}")
311
+
312
+ def check_required_paths(self) -> bool:
313
+ return True
@@ -117,8 +117,6 @@ class TaskScheduler:
117
117
  from_steps=set([step for step, _ in self.__flow.get_entry_nodes()]),
118
118
  prune_nodes=self.__project.option.get_prune())
119
119
 
120
- init_funcs = set()
121
-
122
120
  for step, index in self.__runtime_flow.get_nodes():
123
121
  if self.__record.get('status', step=step, index=index) != NodeStatus.PENDING:
124
122
  continue
@@ -145,7 +143,6 @@ class TaskScheduler:
145
143
  task["node"].set_queue(pipe, self.__log_queue)
146
144
 
147
145
  task["proc"] = multiprocessing.Process(target=task["node"].run)
148
- init_funcs.add(task["node"].init)
149
146
  self.__nodes[(step, index)] = task
150
147
 
151
148
  # Create ordered list of nodes
@@ -155,10 +152,6 @@ class TaskScheduler:
155
152
  if node in self.__nodes:
156
153
  self.__ordered_nodes.append(node)
157
154
 
158
- # Call preprocessing for schedulers
159
- for init_func in init_funcs:
160
- init_func(self.__project)
161
-
162
155
  def run(self, job_log_handler: logging.Handler) -> None:
163
156
  """
164
157
  The main entry point for the task scheduling loop.
@@ -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
@@ -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
  """
@@ -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)
siliconcompiler/tool.py CHANGED
@@ -130,7 +130,8 @@ class Task(NamedSchema, PathSchema, DocsSchema):
130
130
  r"^\s*" + __parse_version_check_str + r"\s*$",
131
131
  re.VERBOSE | re.IGNORECASE)
132
132
 
133
- __POLL_INTERVAL: float = 0.1
133
+ __IO_POLL_INTERVAL: float = 0.1
134
+ __MEM_POLL_INTERVAL: float = 0.5
134
135
  __MEMORY_WARN_LIMIT: int = 90
135
136
 
136
137
  def __init__(self):
@@ -415,7 +416,7 @@ class Task(NamedSchema, PathSchema, DocsSchema):
415
416
  cmdlist.extend(veropt)
416
417
 
417
418
  self.logger.debug(f'Running {self.tool()}/{self.task()} version check: '
418
- f'{" ".join(cmdlist)}')
419
+ f'{shlex.join(cmdlist)}')
419
420
 
420
421
  proc = subprocess.run(cmdlist,
421
422
  stdin=subprocess.DEVNULL,
@@ -900,6 +901,37 @@ class Task(NamedSchema, PathSchema, DocsSchema):
900
901
  f'{timeout} seconds. Terminating...')
901
902
  terminate_process(proc.pid, timeout=timeout)
902
903
 
904
+ def __collect_memory(self, pid) -> Optional[int]:
905
+ try:
906
+ pproc = psutil.Process(pid)
907
+ proc_mem_bytes = pproc.memory_full_info().uss
908
+ for child in pproc.children(recursive=True):
909
+ proc_mem_bytes += child.memory_full_info().uss
910
+ return proc_mem_bytes
911
+ except psutil.Error:
912
+ # Process may have already terminated or been killed.
913
+ # Retain existing memory usage statistics in this case.
914
+ pass
915
+ except PermissionError:
916
+ # OS is preventing access to this information so it cannot
917
+ # be collected
918
+ pass
919
+ return None
920
+
921
+ def __check_memory_limit(self, warn_limit: int) -> int:
922
+ try:
923
+ memory_usage = psutil.virtual_memory()
924
+ if memory_usage.percent > warn_limit:
925
+ self.logger.warning(
926
+ 'Current system memory usage is '
927
+ f'{memory_usage.percent:.1f}%')
928
+ return int(memory_usage.percent + 1)
929
+ except psutil.Error:
930
+ pass
931
+ except PermissionError:
932
+ pass
933
+ return warn_limit
934
+
903
935
  def run_task(self,
904
936
  workdir: str,
905
937
  quiet: bool,
@@ -1034,39 +1066,29 @@ class Task(NamedSchema, PathSchema, DocsSchema):
1034
1066
  raise TaskError(f"Unable to start {exe}: {str(e)}")
1035
1067
 
1036
1068
  memory_warn_limit = Task.__MEMORY_WARN_LIMIT
1069
+ next_collection = None
1037
1070
  try:
1038
1071
  while proc.poll() is None:
1072
+ curr_time = time.time()
1073
+
1039
1074
  # Monitor subprocess memory usage
1040
- try:
1041
- pproc = psutil.Process(proc.pid)
1042
- proc_mem_bytes = pproc.memory_full_info().uss
1043
- for child in pproc.children(recursive=True):
1044
- proc_mem_bytes += child.memory_full_info().uss
1045
- max_mem_bytes = max(max_mem_bytes, proc_mem_bytes)
1046
-
1047
- memory_usage = psutil.virtual_memory()
1048
- if memory_usage.percent > memory_warn_limit:
1049
- self.logger.warning(
1050
- 'Current system memory usage is '
1051
- f'{memory_usage.percent:.1f}%')
1052
- memory_warn_limit = int(memory_usage.percent + 1)
1053
- except psutil.Error:
1054
- # Process may have already terminated or been killed.
1055
- # Retain existing memory usage statistics in this case.
1056
- pass
1057
- except PermissionError:
1058
- # OS is preventing access to this information so it cannot
1059
- # be collected
1060
- pass
1075
+ if next_collection is None or \
1076
+ next_collection <= curr_time:
1077
+ proc_mem_bytes = self.__collect_memory(proc.pid)
1078
+ if proc_mem_bytes is not None:
1079
+ max_mem_bytes = max(max_mem_bytes, proc_mem_bytes)
1080
+ next_collection = curr_time + Task.__MEM_POLL_INTERVAL
1081
+
1082
+ memory_warn_limit = self.__check_memory_limit(memory_warn_limit)
1061
1083
 
1062
1084
  read_stdio(stdout_reader, stderr_reader)
1063
1085
 
1064
1086
  # Check for timeout
1065
- duration = time.time() - cpu_start
1087
+ duration = curr_time - cpu_start
1066
1088
  if timeout is not None and duration > timeout:
1067
1089
  raise TaskTimeout(timeout=duration)
1068
1090
 
1069
- time.sleep(Task.__POLL_INTERVAL)
1091
+ time.sleep(Task.__IO_POLL_INTERVAL)
1070
1092
  except KeyboardInterrupt:
1071
1093
  self.logger.info("Received ctrl-c.")
1072
1094
  self.__terminate_exe(proc)
@@ -172,6 +172,7 @@ class KLayoutTask(ASICTask):
172
172
  with self.active_dataroot("refdir"):
173
173
  self.set_refdir("scripts")
174
174
 
175
+ self.set_environmentalvariable('PYTHONUNBUFFERED', '1')
175
176
  if self.project.get('option', 'nodisplay'):
176
177
  # Tells QT to use the offscreen platform if nodisplay is used
177
178
  self.set_environmentalvariable('QT_QPA_PLATFORM', 'offscreen')
@@ -185,6 +186,8 @@ class KLayoutTask(ASICTask):
185
186
  options = super().runtime_options()
186
187
  options.extend(['-rd', f'SC_KLAYOUT_ROOT={self.find_files("refdir")[0]}'])
187
188
  options.extend(['-rd', f'SC_TOOLS_ROOT={os.path.dirname(os.path.dirname(__file__))}'])
189
+ options.extend(['-rd',
190
+ f'SC_ROOT={os.path.dirname(os.path.dirname(os.path.dirname(__file__)))}'])
188
191
  return options
189
192
 
190
193
  def post_process(self):
@@ -151,6 +151,7 @@ def convert_drc(view, path):
151
151
  def main():
152
152
  # SC_ROOT provided by CLI
153
153
  sys.path.append(SC_KLAYOUT_ROOT) # noqa: F821
154
+ sys.path.append(SC_ROOT) # noqa: F821
154
155
 
155
156
  from klayout_utils import get_schema
156
157
 
@@ -121,6 +121,7 @@ def main():
121
121
  # SC_ROOT provided by CLI
122
122
  sys.path.append(SC_KLAYOUT_ROOT) # noqa: F821
123
123
  sys.path.append(SC_TOOLS_ROOT) # noqa: F821
124
+ sys.path.append(SC_ROOT) # noqa: F821
124
125
 
125
126
  from klayout_utils import (
126
127
  technology,
@@ -326,6 +326,7 @@ if __name__ == "__main__":
326
326
  # SC_ROOT provided by CLI
327
327
  sys.path.append(SC_KLAYOUT_ROOT) # noqa: F821
328
328
  sys.path.append(SC_TOOLS_ROOT) # noqa: F821
329
+ sys.path.append(SC_ROOT) # noqa: F821
329
330
 
330
331
  from klayout_utils import (
331
332
  technology,
@@ -193,6 +193,7 @@ def main():
193
193
  # SC_ROOT provided by CLI, and is only accessible when this is main module
194
194
  sys.path.append(SC_KLAYOUT_ROOT) # noqa: F821
195
195
  sys.path.append(SC_TOOLS_ROOT) # noqa: F821
196
+ sys.path.append(SC_ROOT) # noqa: F821
196
197
 
197
198
  from klayout_utils import (
198
199
  technology,
@@ -1,7 +1,6 @@
1
1
  import pya
2
2
  import json
3
3
  import shutil
4
- import sys
5
4
  import os.path
6
5
 
7
6
 
@@ -129,8 +128,9 @@ def technology(design, schema):
129
128
  map_file = os.path.abspath(os.path.join(os.path.dirname(tech_file),
130
129
  map_file))
131
130
  for s in get_streams(schema):
132
- if schema.valid('library', sc_pdk, 'layermapfileset', 'klayout', 'def', s):
133
- for fileset in schema.get('library', sc_pdk, 'layermapfileset', 'klayout', 'def', s):
131
+ if schema.valid('library', sc_pdk, 'pdk', 'layermapfileset', 'klayout', 'def', s):
132
+ for fileset in schema.get('library', sc_pdk, 'pdk', 'layermapfileset', 'klayout',
133
+ 'def', s):
134
134
  if schema.valid('library', sc_pdk, "fileset", fileset, "file", "layermap"):
135
135
  map_file = schema.get('library', sc_pdk, "fileset", fileset, "file", "layermap")
136
136
  if map_file:
@@ -170,7 +170,6 @@ def get_write_options(filename, timestamps):
170
170
 
171
171
 
172
172
  def get_schema(manifest):
173
- sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
174
173
  from schema.safeschema import SafeSchema
175
174
  return SafeSchema.from_manifest(filepath=manifest)
176
175