PyLTSpice 5.3.2__tar.gz → 5.4.2__tar.gz

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 (85) hide show
  1. {pyltspice-5.3.2 → pyltspice-5.4.2}/PKG-INFO +30 -7
  2. pyltspice-5.4.2/PyLTSpice/LTSpiceBatch.py +9 -0
  3. pyltspice-5.4.2/PyLTSpice/LTSpice_RawRead.py +10 -0
  4. pyltspice-5.4.2/PyLTSpice/LTSpice_RawWrite.py +10 -0
  5. pyltspice-5.4.2/PyLTSpice/client_server/sim_client.py +208 -0
  6. pyltspice-5.4.2/PyLTSpice/client_server/sim_server.py +130 -0
  7. pyltspice-5.4.2/PyLTSpice/client_server/srv_sim_runner.py +123 -0
  8. pyltspice-5.4.2/PyLTSpice/raw/raw_convert.py +141 -0
  9. pyltspice-5.4.2/PyLTSpice/sim/__init__.py +0 -0
  10. pyltspice-5.4.2/PyLTSpice/sim/ngspice_simulator.py +106 -0
  11. pyltspice-5.4.2/PyLTSpice/sim/run_task.py +174 -0
  12. pyltspice-5.4.2/PyLTSpice/sim/sim_analysis.py +105 -0
  13. pyltspice-5.4.2/PyLTSpice/sim/simulator.py +123 -0
  14. pyltspice-5.4.2/PyLTSpice/sim/spice_editor.py +1067 -0
  15. pyltspice-5.4.2/PyLTSpice/sim/xyce_simulator.py +135 -0
  16. pyltspice-5.4.2/PyLTSpice/sim_batch.py +488 -0
  17. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/PKG-INFO +30 -7
  18. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/SOURCES.txt +32 -0
  19. pyltspice-5.4.2/PyLTSpice.egg-info/requires.txt +1 -0
  20. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/top_level.txt +1 -0
  21. {pyltspice-5.3.2 → pyltspice-5.4.2}/README.md +26 -3
  22. pyltspice-5.4.2/examples/issue126/testcase/test.py +6 -0
  23. {pyltspice-5.3.2 → pyltspice-5.4.2}/pyproject.toml +3 -3
  24. pyltspice-5.4.2/tests/batch_test.py +50 -0
  25. pyltspice-5.4.2/tests/batch_test2.py +31 -0
  26. pyltspice-5.4.2/tests/batch_test3.py +70 -0
  27. pyltspice-5.4.2/tests/batch_test4.py +65 -0
  28. pyltspice-5.4.2/tests/batch_test_with_abort.py +48 -0
  29. pyltspice-5.4.2/tests/fra_example.py +58 -0
  30. pyltspice-5.4.2/tests/ngspice_batch.py +30 -0
  31. pyltspice-5.4.2/tests/raw_plotting.py +109 -0
  32. pyltspice-5.4.2/tests/raw_write_tests.py +108 -0
  33. pyltspice-5.4.2/tests/raw_write_tools.py +111 -0
  34. pyltspice-5.4.2/tests/rc_example.py +9 -0
  35. pyltspice-5.4.2/tests/sim_server_example.py +10 -0
  36. pyltspice-5.4.2/tests/sketch.py +54 -0
  37. pyltspice-5.4.2/tests/test_ltsteps.py +20 -0
  38. pyltspice-5.4.2/tests/unittest/sweep_iterators/sweep_iterators_unittest.py +147 -0
  39. pyltspice-5.4.2/tests/unittest/test_pyltspice.py +497 -0
  40. pyltspice-5.3.2/PyLTSpice.egg-info/requires.txt +0 -1
  41. {pyltspice-5.3.2 → pyltspice-5.4.2}/LICENSE +0 -0
  42. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/Histogram.py +0 -0
  43. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/LTSteps.py +0 -0
  44. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/__init__.py +0 -0
  45. {pyltspice-5.3.2/PyLTSpice/editor → pyltspice-5.4.2/PyLTSpice/client_server}/__init__.py +0 -0
  46. {pyltspice-5.3.2/PyLTSpice/log → pyltspice-5.4.2/PyLTSpice/editor}/__init__.py +0 -0
  47. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/editor/asc_editor.py +0 -0
  48. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/editor/spice_editor.py +0 -0
  49. {pyltspice-5.3.2/PyLTSpice/raw → pyltspice-5.4.2/PyLTSpice/log}/__init__.py +0 -0
  50. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/log/logfile_data.py +0 -0
  51. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/log/ltsteps.py +0 -0
  52. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/log/semi_dev_op_reader.py +0 -0
  53. {pyltspice-5.3.2/PyLTSpice/sim → pyltspice-5.4.2/PyLTSpice/raw}/__init__.py +0 -0
  54. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/raw/raw_classes.py +0 -0
  55. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/raw/raw_read.py +0 -0
  56. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/raw/raw_write.py +0 -0
  57. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/rawplot.py +0 -0
  58. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/run_server.py +0 -0
  59. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/ltspice_simulator.py +0 -0
  60. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/process_callback.py +0 -0
  61. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/sim_batch.py +0 -0
  62. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/sim_runner.py +0 -0
  63. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/sim_stepping.py +0 -0
  64. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/tookit/montecarlo.py +0 -0
  65. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/sim/tookit/worst_case.py +0 -0
  66. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/utils/detect_encoding.py +0 -0
  67. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice/utils/sweep_iterators.py +0 -0
  68. {pyltspice-5.3.2 → pyltspice-5.4.2}/PyLTSpice.egg-info/dependency_links.txt +0 -0
  69. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/ltsteps_example.py +0 -0
  70. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/raw_plotting.py +0 -0
  71. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/raw_read_example.py +0 -0
  72. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/raw_write_example.py +0 -0
  73. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/run_montecarlo.py +0 -0
  74. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/run_worst_case.py +0 -0
  75. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_asc_example.py +0 -0
  76. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_callback_example.py +0 -0
  77. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_callback_process_example.py +0 -0
  78. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_runner_example.py +0 -0
  79. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sim_stepper_example.py +0 -0
  80. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/spice_editor_example.py +0 -0
  81. {pyltspice-5.3.2 → pyltspice-5.4.2}/examples/sub_circuit_asc_edits.py +0 -0
  82. {pyltspice-5.3.2 → pyltspice-5.4.2}/setup.cfg +0 -0
  83. {pyltspice-5.3.2 → pyltspice-5.4.2}/unittests/sweep_iterators_unittest.py +0 -0
  84. {pyltspice-5.3.2 → pyltspice-5.4.2}/unittests/test_asc_editor.py +0 -0
  85. {pyltspice-5.3.2 → pyltspice-5.4.2}/unittests/test_pyltspice.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: PyLTSpice
3
- Version: 5.3.2
3
+ Version: 5.4.2
4
4
  Summary: A set of tools to Automate LTSpice simulations
5
5
  Author-email: Nuno Brum <me@nunobrum.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -685,10 +685,10 @@ Project-URL: Author, https://www.nunobrum.com/
685
685
  Classifier: Programming Language :: Python :: 3
686
686
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
687
687
  Classifier: Operating System :: OS Independent
688
- Requires-Python: >=3.8
688
+ Requires-Python: >=3.9
689
689
  Description-Content-Type: text/markdown
690
690
  License-File: LICENSE
691
- Requires-Dist: spicelib>=1.2.1
691
+ Requires-Dist: spicelib>=1.4.1
692
692
 
693
693
  # README #
694
694
 
@@ -1145,11 +1145,34 @@ _Make sure to initialize the root logger before importing the library to be able
1145
1145
 
1146
1146
  ## To whom do I talk to? ##
1147
1147
 
1148
- * Tools website : [https://www.nunobrum.com/pyltspice.html](https://www.nunobrum.com/pyltspice.html)
1149
- * Repo owner : [me@nunobrum.com](mailto:me@nunobrum.com)
1150
- * Alternative contact : [nuno.brum@gmail.com](mailto:nuno.brum@gmail.com)
1148
+ For support and improvement requests please open an Issue in [GitHub spicelib issues](https://github.com/nunobrum/spicelib/issues)
1151
1149
 
1152
1150
  ## History ##
1151
+ * Version 5.4.2
1152
+ * Imported changes from spicelib 1.4.1 (Summary)
1153
+ * Added `get_all_parameter_names()` function to all editors (#159)
1154
+ * Compatibility with LTspice 24+
1155
+ * Fixed Problem with .PARAM regex.
1156
+ * Documenting the user library paths
1157
+ * AscEditor: Adding support to DATAFLAG
1158
+ * LTSteps: Supporting new LTspice data export format
1159
+ * Version 5.4.1
1160
+ * Fixing Issue #146
1161
+ * Version 5.4.0
1162
+ * Adding possibility of manipulating parameters on sub-circuits
1163
+ * Supporting subcircuit names with dots.
1164
+ * Overall documentation improvements (thanks @hb020)
1165
+ * Major improvement in Documentation
1166
+ * Introduced a read-only property that blocks libraries from being updated.
1167
+ * Support for LTspice log files with the option : expanded netlist
1168
+ * Supporting library symbols using BLOCK primitive
1169
+ * Improved unittest on the .ASC hierarchical design
1170
+ * SimRunner simulation iterator only returns successful simulations in order to simplify error management
1171
+ * In QschEditor, the replacement of unique dot instructions (ex: .TRAN .AC .NOISE) is only done if the existing instruction is not commented.
1172
+ * RunTask.get_results() now returns None if a callback function is provided and the simulation has failed.
1173
+ * BigFix: Inclusion of encrypted libraries would crash
1174
+ * Bugfix: Prefix were case sensitive in SpiceEditor
1175
+ * Bugfix: Parsing netlists with extensions other than .net didn't work properly
1153
1176
  * Version 5.3.2
1154
1177
  * Correction on the readthedocs webhook configuration
1155
1178
  * Alignement with the spicelib 1.2.1
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ *(Deprecated)*
4
+ Supporting Legacy import clauses. This will disappear in future versions
5
+ """
6
+ print("Deprecation Warning! This will no longer be supported in future versions.\n"
7
+ "Use 'from PyLTSpice import SimCommander' for a direct import of the LTSpice/NGSpice batch run class.")
8
+
9
+ from .sim.sim_batch import SimCommander
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ *(Deprecated)*
4
+ Supporting Legacy import clauses. This will disappear in future versions
5
+ """
6
+
7
+ print("Deprecation Warning! This will no longer be supported in future versions.\n"
8
+ "Use 'from PyLTSpice import RawWRead' for a direct import of the Raw Reading class")
9
+
10
+ from .raw.raw_read import *
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ *(Deprecated)*
4
+ Supporting Legacy import clauses. This will disappear in future versions
5
+ """
6
+
7
+ print("Deprecation Warning! This will no longer be supported in future versions.\n"
8
+ "Use 'from PyLTSpice import RawWRead' for a direct import of the Raw Write class")
9
+
10
+ from .raw.raw_write import *
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+
4
+ # -------------------------------------------------------------------------------
5
+ # ____ _ _____ ____ _
6
+ # | _ \ _ _| | |_ _/ ___| _ __ (_) ___ ___
7
+ # | |_) | | | | | | | \___ \| '_ \| |/ __/ _ \
8
+ # | __/| |_| | |___| | ___) | |_) | | (_| __/
9
+ # |_| \__, |_____|_| |____/| .__/|_|\___\___|
10
+ # |___/ |_|
11
+ #
12
+ # Name: sim_client.py
13
+ # Purpose: Tool used to launch LTSpice simulation in batch mode.
14
+ #
15
+ # Author: Nuno Brum (nuno.brum@gmail.com)
16
+ #
17
+ # Created: 23-02-2023
18
+ # Licence: refer to the LICENSE file
19
+ # -------------------------------------------------------------------------------
20
+ import os.path
21
+ import zipfile
22
+ import xmlrpc.client
23
+ import io
24
+ import pathlib
25
+ import time
26
+ from collections import OrderedDict
27
+ from dataclasses import dataclass
28
+ import logging
29
+ _logger = logging.getLogger("PyLTSpice.SimClient")
30
+
31
+ class SimClientInvalidRunId(LookupError):
32
+ """Raised when asking for a run_no that doesn't exist"""
33
+ ...
34
+
35
+ @dataclass
36
+ class JobInformation:
37
+ """Contains information about pending simulation jobs"""
38
+ run_number: int # The run id that is returned by the Server and which identifies the server
39
+ file_dir : pathlib.Path
40
+
41
+ # class RunIterator(object):
42
+ #
43
+ # def __init__(self, client, timeout):
44
+ # self.client = client
45
+ # self.timeout = timeout
46
+ #
47
+ # def __iter__(self):
48
+ # return self
49
+ #
50
+ # def __next__(self):
51
+ # return self.client.__next__()
52
+
53
+
54
+ class SimClient(object):
55
+ """
56
+ Class used for launching simulations in a Spice Simulation Server.
57
+ A Spice Simulation Server is a machine running a script with an active SimServer object.
58
+
59
+ This class only implement basic level handshaking with a single simulation Server.
60
+ Upon instance, it will establish a connection with Simulation Server. This connection is kept
61
+ alive during the whole live of this object.
62
+
63
+ The run() method will transfer the netlist for the server, execute a simulation and transfer the simulation results
64
+ back to the client.
65
+
66
+ Data is returned from the server inside a zipfie which is copied into the directory defined when the job was
67
+ created, /i.e./ run() method called.
68
+
69
+ Two lists are kept by this class:
70
+
71
+ * A list of started jobs (started_jobs) and,
72
+
73
+ * a list with finished jobs on the server, but, which haven't been yet transferred to the client (stored_jobs).
74
+
75
+ This distinction is important because the data is erased on the server side when the data is transferred.
76
+
77
+ This class implements an iterator that is to be used for retrieving the job. See the example below.
78
+ The iterator polls the server with a time interval defined by the attribute ``minimum_time_between_server_calls``.
79
+ This attribute is set to 0.2 seconds by default, but it can be overriden.
80
+
81
+ Usage:
82
+
83
+ .. code-block:: python
84
+
85
+ import zipfile
86
+ from PySpice.sim.sim_client import SimClient
87
+
88
+ server = SimClient('http://localhost', 9000) # Use another computer address.
89
+ print(server.session_id)
90
+ runid = server.run("../../tests/testfile.net")
91
+ print("Got Job id", runid)
92
+
93
+ for runid in server: # may not arrive in the same order as runids were launched
94
+ zip_filename = server.get_runno_data(runid)
95
+ print(f"Received {zip_filename} from runid {runid}")
96
+
97
+ with zipfile.ZipFile(zip_filename, 'r') as zipf: # Extract the contents of the zip file
98
+ print(zipf.namelist()) # Debug printing the contents of the zip file
99
+ zipf.extract(zipf.namelist()[0]) # Normally the raw file comes first
100
+
101
+ NOTE: More elaborate algorithms such as managing multiple servers will be done on another class.
102
+ """
103
+
104
+ def __init__(self, host_address, port):
105
+ self.server = xmlrpc.client.ServerProxy(f'{host_address}:{port}')
106
+ # print(self.server.system.listMethods())
107
+ self.session_id = self.server.start_session()
108
+ _logger.info("started ", self.session_id)
109
+ self.started_jobs = OrderedDict() # This list keeps track of started jobs on the server
110
+ self.stored_jobs = OrderedDict() # This list keeps track of finished simulations that haven't yet been transferred.
111
+ self.completed_jobs = 0
112
+ self.minimum_time_between_server_calls = 0.2 # Minimum time between server calls
113
+ self._last_server_call = time.clock()
114
+
115
+ def __del__(self):
116
+ _logger.info("closing session ", self.session_id)
117
+ self.server.close_session(self.session_id)
118
+
119
+ def run(self, circuit):
120
+ """
121
+ Sends the netlist identified with the argument "circuit" to the server, and it receives a run identifier
122
+ (runno). Since the server can receive requests from different machines, this identifier is not guaranteed to be
123
+ sequential.
124
+
125
+ :param circuit: path to the netlist file containing the simulation directives.
126
+ :type circuit: pathlib.Path or str
127
+ :returns: identifier on the server of the simulation.
128
+ :rtype: int
129
+ """
130
+ circuit_path = pathlib.Path(circuit)
131
+ circuit_name = circuit_path.name
132
+ if os.path.exists(circuit):
133
+ # Create a buffer to store the zip file in memory
134
+ zip_buffer = io.BytesIO()
135
+
136
+ # Create the zip file in memory
137
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
138
+ zip_file.write(circuit, circuit_name) # Makes sure it writes it to the root of the zipfile
139
+
140
+ # Reset the buffer position to the start
141
+ zip_buffer.seek(0)
142
+
143
+ # Read the zip file from the buffer and send it to the server
144
+ zip_data = zip_buffer.read()
145
+
146
+ run_id = self.server.run(self.session_id, circuit_name, zip_data)
147
+ job_info = JobInformation(run_number=run_id, file_dir=circuit_path.parent)
148
+ self.started_jobs[job_info] = job_info
149
+ return run_id
150
+ else:
151
+ _logger.error(f"Circuit {circuit} doesn't exit")
152
+ return -1
153
+
154
+ def get_runno_data(self, runno) -> str:
155
+ """
156
+ Returns the simulation output data inside a zip file name.
157
+
158
+ :rtype: str
159
+ """
160
+ if runno not in self.stored_jobs:
161
+ raise SimClientInvalidRunId(f"Invalid Job id {runno}")
162
+
163
+ zip_filename, zipdata = self.server.get_files(self.session_id, runno)
164
+ job = self.stored_jobs.pop(runno) # Removes it from stored jobs
165
+ self.completed_jobs += 1
166
+ if zip_filename != '':
167
+ store_path = job.file_dir
168
+ with open(store_path / zip_filename, 'wb') as f:
169
+ f.write(zipdata.data)
170
+ return zip_filename
171
+
172
+ def __iter__(self):
173
+ return self
174
+
175
+ def __next__(self):
176
+ while len(self.started_jobs) > 0:
177
+ status = self.server.status(self.session_id)
178
+ if len(status) > 0:
179
+ runno = status.pop(0)
180
+ self.stored_jobs[runno] = self.started_jobs.pop(runno) # Job is taken out of the started jobs list and
181
+ # is added to the stored jobs
182
+ return runno
183
+ else:
184
+ now = time.clock()
185
+ delta = self.minimum_time_between_server_calls - (now - self._last_server_call)
186
+ if delta > 0:
187
+ time.sleep(delta) # Go asleep for a sec
188
+ self._last_server_call = now
189
+
190
+ # when there are no pending jobs left, exit the iterator
191
+ raise StopIteration
192
+
193
+
194
+ if __name__ == "__main__":
195
+
196
+ server = SimClient('http://localhost', 9000)
197
+ print(server.session_id)
198
+ runid = server.run("../../tests/testfile.net")
199
+ print("Got Job id", runid)
200
+ for runid in server: # Ma
201
+ zip_filename = server.get_runno_data(runid)
202
+ print(f"Received {zip_filename} from runid {runid}")
203
+ with zipfile.ZipFile(zip_filename, 'r') as zipf: # Extract the contents of the zip file
204
+ print(zipf.namelist()) # Debug printing the contents of the zip file
205
+ zipf.extract(zipf.namelist()[0]) # Normally the raw file comes first
206
+
207
+ print("Finished")
208
+ server.server.stop_server() # This will terminate the server
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+
4
+ # -------------------------------------------------------------------------------
5
+ # ____ _ _____ ____ _
6
+ # | _ \ _ _| | |_ _/ ___| _ __ (_) ___ ___
7
+ # | |_) | | | | | | | \___ \| '_ \| |/ __/ _ \
8
+ # | __/| |_| | |___| | ___) | |_) | | (_| __/
9
+ # |_| \__, |_____|_| |____/| .__/|_|\___\___|
10
+ # |___/ |_|
11
+ #
12
+ # Name: sim_server.py
13
+ # Purpose: Tool used to launch LTSpice simulation in batch mode.
14
+ #
15
+ # Author: Nuno Brum (nuno.brum@gmail.com)
16
+ #
17
+ # Created: 23-02-2023
18
+ # Licence: refer to the LICENSE file
19
+ # -------------------------------------------------------------------------------
20
+ from typing import Tuple
21
+ from xmlrpc.client import Binary
22
+ from xmlrpc.server import SimpleXMLRPCServer
23
+ import logging
24
+ _logger = logging.getLogger("PyLTSpice.SimServer")
25
+
26
+ import threading
27
+ import zipfile
28
+ import io
29
+ from PyLTSpice.client_server.srv_sim_runner import ServerSimRunner
30
+ import uuid
31
+
32
+
33
+ class SimServer():
34
+
35
+ def __init__(self, simulator, parallel_sims=4, output_folder='./temp1', port=9000):
36
+ self.output_folder = output_folder
37
+ self.simulation_manager = ServerSimRunner(parallel_sims=parallel_sims, timeout=5 * 60, verbose=True,
38
+ output_folder=output_folder, simulator=simulator)
39
+ self.server = SimpleXMLRPCServer(
40
+ ('localhost', port),
41
+ # requestHandler=RequestHandler
42
+ )
43
+ self.server.register_introspection_functions()
44
+ self.server.register_instance(self)
45
+ self.sessions = {} # this will contain the session_id ids hashing their respective list of sim_tasks
46
+ self.simulation_manager.start()
47
+ self.server_thread = threading.Thread(target=self.server.serve_forever, name="ServerThread")
48
+ self.server_thread.start()
49
+
50
+ def run(self, session_id, circuit_name, zip_data):
51
+ _logger.info("Run ", session_id, circuit_name)
52
+ if session_id not in self.sessions:
53
+ return -1 # This indicates that no job is started
54
+ # Create a buffer from the zip data
55
+ zip_buffer = io.BytesIO(zip_data.data)
56
+ _logger.debug("Created the buffer")
57
+ # Extract the contents of the zip file
58
+ with zipfile.ZipFile(zip_buffer, 'r') as zip_file:
59
+ _logger.debug(zip_file.namelist())
60
+ zip_file.extract(circuit_name, self.output_folder)
61
+
62
+ _logger.info(f"Running simulation of {circuit_name}")
63
+ runno = self.simulation_manager.add_simulation(circuit_name)
64
+ if runno != -1:
65
+ self.sessions[session_id].append(runno)
66
+ return runno
67
+
68
+ def start_session(self):
69
+ """Returns an unique key that represents the session. It will be later used to sort the sim_tasks belonging
70
+ to the session."""
71
+ session_id = str(uuid.uuid4()) # Needs to be a string, otherwise the rpc client can't handle it
72
+ _logger.info("Starting session ", session_id)
73
+ self.sessions[session_id] = []
74
+ return session_id
75
+
76
+ def status(self, session_id):
77
+ """
78
+ Returns a dictionary with task information. The key for the dictionary is the simulation identifier returned
79
+ by the simulation start command. The value associated with each simulation identifier is another dictionary
80
+ containing the following keys:
81
+
82
+ * 'completed' - whether the simulation is already finished
83
+
84
+ * 'start' - time when the simulation was started
85
+
86
+ * 'stop' - server time
87
+ """
88
+ _logger.debug("collecting status for ", session_id)
89
+ ret = []
90
+ for task_info in self.simulation_manager.completed_tasks:
91
+ _logger.debug(task_info)
92
+ runno = task_info['runno']
93
+ if runno in self.sessions[session_id]:
94
+ ret.append(runno) # transfers the dictionary from the simulation_manager completed task
95
+ # to the return dictionary
96
+ _logger.debug("returning status", ret)
97
+ return ret
98
+
99
+ def get_files(self, session_id, runno) -> Tuple[str, Binary]:
100
+ if runno in self.sessions[session_id]:
101
+
102
+ for task_info in self.simulation_manager.completed_tasks:
103
+ if runno == task_info['runno']:
104
+ # Create a buffer to store the zip file in memory
105
+ zip_file = task_info['zipfile']
106
+ zip = zip_file.open('rb')
107
+ # Read the zip file from the buffer and send it to the server
108
+ zip_data = zip.read()
109
+ zip.close()
110
+ self.simulation_manager.erase_files_of_runno(runno)
111
+ return zip_file.name, Binary(zip_data)
112
+
113
+ return "", Binary(b'') # Returns and empty data
114
+
115
+ def close_session(self, session_id):
116
+ """Cleans all the pending sim_tasks with """
117
+ for runno in self.sessions[session_id]:
118
+ self.simulation_manager.erase_files_of_runno(runno)
119
+ return True # Needs to return always something. None is not supported
120
+
121
+ def stop_server(self):
122
+ _logger.info("stopping...ServerInterface")
123
+ self.simulation_manager.stop()
124
+ self.server.shutdown()
125
+ _logger.info("stopped...ServerInterface")
126
+ return True # Needs to return always something. None is not supported
127
+
128
+ def running(self):
129
+ return self.simulation_manager.running()
130
+
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python
2
+ # coding=utf-8
3
+
4
+ # -------------------------------------------------------------------------------
5
+ # ____ _ _____ ____ _
6
+ # | _ \ _ _| | |_ _/ ___| _ __ (_) ___ ___
7
+ # | |_) | | | | | | | \___ \| '_ \| |/ __/ _ \
8
+ # | __/| |_| | |___| | ___) | |_) | | (_| __/
9
+ # |_| \__, |_____|_| |____/| .__/|_|\___\___|
10
+ # |___/ |_|
11
+ #
12
+ # Name: srv_sim_runner.py
13
+ # Purpose: Manager of the simulation sim_tasks on the server side
14
+ #
15
+ # Author: Nuno Brum (nuno.brum@gmail.com)
16
+ #
17
+ # Created: 23-02-2023
18
+ # Licence: refer to the LICENSE file
19
+ # -------------------------------------------------------------------------------
20
+ import threading
21
+ import time
22
+ from typing import Any, Callable, Union
23
+ from pathlib import Path
24
+ import zipfile
25
+ import logging
26
+ _logger = logging.getLogger("PyLTSpice.ServerSimRunner")
27
+
28
+ from PyLTSpice.sim.sim_runner import SimRunner
29
+ from PyLTSpice.sim.spice_editor import SpiceEditor
30
+
31
+
32
+ def zip_files(raw_filename: Path, log_filename:Path):
33
+ zip_filename = raw_filename.with_suffix('.zip')
34
+ with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zip_file:
35
+ zip_file.write(raw_filename)
36
+ zip_file.write(log_filename)
37
+ return zip_filename
38
+
39
+
40
+ class ServerSimRunner(threading.Thread):
41
+ """This class maintains updated status of the SimRunner.
42
+ It was decided not to make SimRunner a super class and rather make it manipulate directly the structures of
43
+ SimRunner. The rationale for this, was to avoid confusions between the run() on the Thread class and the
44
+ run on the SimRunner class.
45
+ Making a class derive from two different classes needs to be handled carefully.
46
+
47
+ In consequence of the rationale above, many of the functions that were handled by the SimRunner are overriden
48
+ by this class.
49
+ """
50
+
51
+ def __init__(self, parallel_sims: int = 4, timeout=None, verbose=True, output_folder: str = None, simulator=None):
52
+ super().__init__(name="SimManager")
53
+ self.runner = SimRunner(parallel_sims, timeout, verbose, output_folder, simulator)
54
+ self.completed_tasks = []
55
+ self._stop = False
56
+
57
+ def run(self) -> None:
58
+ """This function makes a direct manipulation of the structures of SimRunner. This option is """
59
+ while True:
60
+ i = 0
61
+ while i < len(self.runner.sim_tasks):
62
+ task = self.runner.sim_tasks[i]
63
+ if task.is_alive():
64
+ i += 1
65
+ else:
66
+ zip_filename = task.callback_return
67
+ self.completed_tasks.append({
68
+ 'runno': task.runno,
69
+ 'retcode': task.retcode,
70
+ 'raw': task.raw_file,
71
+ 'log': task.log_file,
72
+ 'zipfile': zip_filename,
73
+ 'start': task.start_time,
74
+ 'stop': task.stop_time,
75
+ })
76
+ _logger.debug(task, "is finished")
77
+ del self.runner.sim_tasks[i]
78
+ _logger.debug(self.completed_tasks[-1])
79
+ _logger.debug(len(self.completed_tasks))
80
+
81
+ time.sleep(0.2)
82
+ if self._stop is True:
83
+ break
84
+ self.runner.wait_completion()
85
+ self.runner.file_cleanup()
86
+
87
+ def add_simulation(self, netlist: Union[str, Path, SpiceEditor], *, timeout: float = 600) -> int:
88
+ """"""
89
+ _logger.debug("starting ", netlist)
90
+ task = self.runner.run(netlist, wait_resource=True, timeout=timeout, callback=zip_files)
91
+ if task is None:
92
+ _logger.error("Failed to start task ", netlist)
93
+ return -1
94
+ else:
95
+ _logger.info("Started task ", netlist, " with job_id", task.runno)
96
+ return task.runno
97
+
98
+ def _erase_files_and_info(self, pos):
99
+ task = self.completed_tasks[pos]
100
+ for filename in ('log', 'raw', 'zipfile'):
101
+ f = task[filename]
102
+ if f.exists():
103
+ f.unlink()
104
+ del self.completed_tasks[pos]
105
+
106
+ def erase_files_of_runno(self, runno):
107
+ """Will delete all files related with a completed task. Will also delete information on the completed_tasks
108
+ attribute."""
109
+ for i, task_info in enumerate(self.completed_tasks):
110
+ if task_info['runno'] == runno:
111
+ self._erase_files_and_info(i)
112
+ break
113
+
114
+ def cleanup_completed(self):
115
+ while len(self.completed_tasks):
116
+ self._erase_files_and_info(0)
117
+
118
+ def stop(self):
119
+ _logger.info("stopping...ServerSimRunner")
120
+ self._stop = True
121
+
122
+ def running(self):
123
+ return self._stop is False